|
// 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
extern alias WORKSPACES;
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.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.DecompiledSource;
using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
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.Editor.UnitTests.Workspaces
{
using RelativePathResolver = WORKSPACES::Microsoft.CodeAnalysis.RelativePathResolver;
public partial class TestWorkspace
{
/// <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);
}
public static TestWorkspace Create(string xmlDefinition, bool openDocuments = false, TestComposition composition = null)
=> Create(XElement.Parse(xmlDefinition), openDocuments, composition);
public static TestWorkspace CreateWorkspace(
XElement workspaceElement,
bool openDocuments = true,
TestComposition composition = null,
string workspaceKind = null)
{
return Create(workspaceElement, openDocuments, composition, workspaceKind);
}
internal static TestWorkspace Create(
XElement workspaceElement,
bool openDocuments = true,
TestComposition composition = null,
string workspaceKind = null,
IDocumentServiceProvider documentServiceProvider = null,
bool ignoreUnchangeableDocumentsWhenApplyingChanges = true)
{
var workspace = new TestWorkspace(composition, workspaceKind, ignoreUnchangeableDocumentsWhenApplyingChanges: ignoreUnchangeableDocumentsWhenApplyingChanges);
workspace.InitializeDocuments(workspaceElement, openDocuments, documentServiceProvider);
return workspace;
}
internal void InitializeDocuments(
string language,
CompilationOptions compilationOptions = null,
ParseOptions parseOptions = null,
string[] files = null,
string[] metadataReferences = null,
string extension = null,
bool commonReferences = true,
bool openDocuments = true,
IDocumentServiceProvider documentServiceProvider = null)
{
var workspaceElement = CreateWorkspaceElement(
language,
compilationOptions,
parseOptions,
files,
sourceGeneratedFiles: Array.Empty<string>(),
metadataReferences,
extension,
commonReferences);
InitializeDocuments(workspaceElement, openDocuments, documentServiceProvider);
}
internal void InitializeDocuments(
XElement workspaceElement,
bool openDocuments = true,
IDocumentServiceProvider documentServiceProvider = null)
{
if (workspaceElement.Name != WorkspaceElementName)
{
throw new ArgumentException();
}
var projectNameToTestHostProject = new Dictionary<string, TestHostProject>();
var projectElementToProjectName = new Dictionary<XElement, string>();
var projectIdentifier = 0;
var documentIdentifier = 0;
foreach (var projectElement in workspaceElement.Elements(ProjectElementName))
{
var project = CreateProject(
workspaceElement,
projectElement,
ExportProvider,
this,
documentServiceProvider,
ref projectIdentifier,
ref documentIdentifier);
Assert.False(projectNameToTestHostProject.ContainsKey(project.Name), $"The workspace XML already contains a project with name {project.Name}");
projectNameToTestHostProject.Add(project.Name, project);
projectElementToProjectName.Add(projectElement, project.Name);
Projects.Add(project);
}
var documentFilePaths = new HashSet<string>();
foreach (var project in projectNameToTestHostProject.Values)
{
foreach (var document in project.Documents)
{
Assert.True(document.IsLinkFile || documentFilePaths.Add(document.FilePath));
Documents.Add(document);
}
}
var submissions = CreateSubmissions(workspaceElement.Elements(SubmissionElementName), ExportProvider);
foreach (var submission in submissions)
{
projectNameToTestHostProject.Add(submission.Name, submission);
Documents.Add(submission.Documents.Single());
}
var solution = new TestHostSolution(projectNameToTestHostProject.Values.ToArray());
AddTestSolution(solution);
foreach (var projectElement in workspaceElement.Elements(ProjectElementName))
{
foreach (var projectReference in projectElement.Elements(ProjectReferenceElementName))
{
var fromName = projectElementToProjectName[projectElement];
var toName = projectReference.Value;
var fromProject = projectNameToTestHostProject[fromName];
var toProject = projectNameToTestHostProject[toName];
var aliases = projectReference.Attributes(AliasAttributeName).Select(a => a.Value).ToImmutableArray();
OnProjectReferenceAdded(fromProject.Id, new ProjectReference(toProject.Id, aliases.Any() ? aliases : default));
}
}
for (var i = 1; i < submissions.Count; i++)
{
if (submissions[i].CompilationOptions == null)
{
continue;
}
for (var j = i - 1; j >= 0; j--)
{
if (submissions[j].CompilationOptions != null)
{
OnProjectReferenceAdded(submissions[i].Id, new ProjectReference(submissions[j].Id));
break;
}
}
}
foreach (var project in projectNameToTestHostProject.Values)
{
foreach (var document in project.Documents)
{
if (openDocuments && !document.IsSourceGenerated)
{
// This implicitly opens the document in the workspace by fetching the container.
document.GetOpenTextContainer();
}
}
}
}
private IList<TestHostProject> CreateSubmissions(
IEnumerable<XElement> submissionElements,
ExportProvider exportProvider)
{
var submissions = new List<TestHostProject>();
var submissionIndex = 0;
foreach (var submissionElement in submissionElements)
{
var submissionName = "Submission" + (submissionIndex++);
var languageName = GetLanguage(this, submissionElement);
// The document
var markupCode = submissionElement.NormalizedValue();
MarkupTestFile.GetPositionAndSpans(markupCode,
out var code, out var cursorPosition, out IDictionary<string, ImmutableArray<TextSpan>> spans);
var languageServices = Services.GetLanguageServices(languageName);
// The project
var document = new TestHostDocument(exportProvider, languageServices, code, submissionName, submissionName, cursorPosition, spans, SourceCodeKind.Script);
var documents = new List<TestHostDocument> { document };
if (languageName == NoCompilationConstants.LanguageName)
{
submissions.Add(
new TestHostProject(
languageServices,
compilationOptions: null,
parseOptions: null,
assemblyName: submissionName,
projectName: submissionName,
references: null,
documents: documents,
isSubmission: true));
continue;
}
var metadataService = Services.GetService<IMetadataService>();
var metadataResolver = RuntimeMetadataReferenceResolver.CreateCurrentPlatformResolver(fileReferenceProvider: metadataService.GetReference);
var syntaxFactory = languageServices.GetService<ISyntaxTreeFactoryService>();
var compilationFactory = languageServices.GetService<ICompilationFactoryService>();
var compilationOptions = compilationFactory.GetDefaultCompilationOptions()
.WithOutputKind(OutputKind.DynamicallyLinkedLibrary)
.WithMetadataReferenceResolver(metadataResolver);
var parseOptions = syntaxFactory.GetDefaultParseOptions().WithKind(SourceCodeKind.Script);
var references = CreateCommonReferences(this, submissionElement);
var project = new TestHostProject(
languageServices,
compilationOptions,
parseOptions,
submissionName,
submissionName,
references,
documents,
isSubmission: true);
submissions.Add(project);
}
return submissions;
}
private static TestHostProject CreateProject(
XElement workspaceElement,
XElement projectElement,
ExportProvider exportProvider,
TestWorkspace workspace,
IDocumentServiceProvider documentServiceProvider,
ref int projectId,
ref int documentId)
{
AssertNoChildText(projectElement);
var language = GetLanguage(workspace, projectElement);
var assemblyName = GetAssemblyName(workspace, projectElement, ref projectId);
string filePath;
var projectName = projectElement.Attribute(ProjectNameAttribute)?.Value ?? assemblyName;
if (projectElement.Attribute(FilePathAttributeName) != null)
{
filePath = projectElement.Attribute(FilePathAttributeName).Value;
if (string.Compare(filePath, NullFilePath, StringComparison.Ordinal) == 0)
{
// allow explicit null file path
filePath = null;
}
}
else
{
filePath = projectName +
(language == LanguageNames.CSharp ? ".csproj" :
language == LanguageNames.VisualBasic ? ".vbproj" : ("." + language));
}
var languageServices = workspace.Services.GetLanguageServices(language);
var parseOptions = GetParseOptions(projectElement, language, languageServices);
var compilationOptions = CreateCompilationOptions(workspace, projectElement, language, parseOptions);
var rootNamespace = GetRootNamespace(workspace, compilationOptions, projectElement);
var references = CreateReferenceList(workspace, projectElement);
var analyzers = CreateAnalyzerList(projectElement);
var documents = new List<TestHostDocument>();
var documentElements = projectElement.Elements(DocumentElementName).ToList();
foreach (var documentElement in documentElements)
{
var document = CreateDocument(
workspace,
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(workspace, sourceGeneratedDocumentElement, ref documentId);
var markupCode = sourceGeneratedDocumentElement.NormalizedValue();
MarkupTestFile.GetPositionAndSpans(markupCode,
out var code, out var cursorPosition, out IDictionary<string, ImmutableArray<TextSpan>> spans);
var documentFilePath = typeof(SingleFileTestGenerator).Assembly.GetName().Name + '\\' + typeof(SingleFileTestGenerator).FullName + '\\' + name;
var document = new TestHostDocument(exportProvider, languageServices, code, name, documentFilePath, cursorPosition, spans, generator: testGenerator);
documents.Add(document);
testGenerator.AddSource(code, name);
}
var additionalDocuments = new List<TestHostDocument>();
var additionalDocumentElements = projectElement.Elements(AdditionalDocumentElementName).ToList();
foreach (var additionalDocumentElement in additionalDocumentElements)
{
var document = CreateDocument(
workspace,
workspaceElement,
additionalDocumentElement,
exportProvider,
languageServices,
documentServiceProvider,
ref documentId);
additionalDocuments.Add(document);
}
var analyzerConfigDocuments = new List<TestHostDocument>();
var analyzerConfigElements = projectElement.Elements(AnalyzerConfigDocumentElementName).ToList();
foreach (var analyzerConfigElement in analyzerConfigElements)
{
var document = CreateDocument(
workspace,
workspaceElement,
analyzerConfigElement,
exportProvider,
languageServices,
documentServiceProvider,
ref documentId);
analyzerConfigDocuments.Add(document);
}
return new TestHostProject(languageServices, compilationOptions, parseOptions, assemblyName, projectName, references, documents, additionalDocuments, analyzerConfigDocuments, filePath: filePath, 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 new KeyValuePair<string, string>(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 static string GetAssemblyName(TestWorkspace workspace, XElement projectElement, ref int projectId)
{
var assemblyNameAttribute = projectElement.Attribute(AssemblyNameAttributeName);
if (assemblyNameAttribute != null)
{
return assemblyNameAttribute.Value;
}
var language = GetLanguage(workspace, projectElement);
projectId++;
return language == LanguageNames.CSharp ? "CSharpAssembly" + projectId :
language == LanguageNames.VisualBasic ? "VisualBasicAssembly" + projectId :
language + "Assembly" + projectId;
}
private static string GetLanguage(TestWorkspace workspace, XElement projectElement)
{
var languageAttribute = projectElement.Attribute(LanguageAttributeName);
if (languageAttribute == null)
{
throw new ArgumentException($"{projectElement} is missing a {LanguageAttributeName} attribute.");
}
var languageName = languageAttribute.Value;
if (!workspace.Services.SupportedLanguages.Contains(languageName))
{
throw new ArgumentException(string.Format("Language should be one of '{0}' and it is {1}",
string.Join(", ", workspace.Services.SupportedLanguages),
languageName));
}
return languageName;
}
private static string GetRootNamespace(TestWorkspace workspace, CompilationOptions compilationOptions, XElement projectElement)
{
var rootNamespaceAttribute = projectElement.Attribute(RootNamespaceAttributeName);
if (GetLanguage(workspace, 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 static CompilationOptions CreateCompilationOptions(
TestWorkspace workspace,
XElement projectElement,
string language,
ParseOptions parseOptions)
{
var compilationOptionsElement = projectElement.Element(CompilationOptionsElementName);
return language is LanguageNames.CSharp or LanguageNames.VisualBasic
? CreateCompilationOptions(workspace, language, compilationOptionsElement, parseOptions)
: null;
}
private static CompilationOptions CreateCompilationOptions(TestWorkspace workspace, 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 = workspace.Services.GetLanguageServices(language);
var metadataService = workspace.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(ImmutableArray<string>.Empty, 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 static TestHostDocument CreateDocument(
TestWorkspace workspace,
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(workspace, 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 new TestHostDocument(
exportProvider, languageServiceProvider, code, fileName, fileName, cursorPosition, spans, codeKind, folders, isLinkFile, documentServiceProvider);
}
internal static TestHostDocument CreateDocument(
XElement documentElement,
ExportProvider exportProvider,
HostLanguageServices languageServiceProvider,
ImmutableArray<string> roles)
{
var markupCode = documentElement.NormalizedValue();
var folders = GetFolders(documentElement);
var optionsElement = documentElement.Element(ParseOptionsElementName);
var codeKind = SourceCodeKind.Regular;
if (optionsElement != null)
{
var attr = optionsElement.Attribute(KindAttributeName);
codeKind = attr == null
? SourceCodeKind.Regular
: (SourceCodeKind)Enum.Parse(typeof(SourceCodeKind), attr.Value);
}
MarkupTestFile.GetPositionAndSpans(markupCode,
out var code, out var cursorPosition, out IDictionary<string, ImmutableArray<TextSpan>> spans);
var documentServiceProvider = GetDocumentServiceProvider(documentElement);
return new TestHostDocument(
exportProvider, languageServiceProvider, code, name: string.Empty, filePath: string.Empty, cursorPosition, spans, codeKind, folders, isLinkFile: false, documentServiceProvider, roles: roles);
}
#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 static string GetFileName(
TestWorkspace workspace,
XElement documentElement,
ref int documentId)
{
var filePathAttribute = documentElement.Attribute(FilePathAttributeName);
if (filePathAttribute != null)
{
return filePathAttribute.Value;
}
var language = GetLanguage(workspace, 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 static MetadataReference CreateMetadataReferenceFromSource(TestWorkspace workspace, XElement projectElement, XElement referencedSource)
{
var compilation = CreateCompilation(workspace, 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 static Compilation CreateCompilation(TestWorkspace workspace, XElement referencedSource)
{
AssertNoChildText(referencedSource);
var languageName = GetLanguage(workspace, referencedSource);
var assemblyName = "ReferencedAssembly";
var assemblyNameAttribute = referencedSource.Attribute(AssemblyNameAttributeName);
if (assemblyNameAttribute != null)
{
assemblyName = assemblyNameAttribute.Value;
}
var languageServices = workspace.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(workspace, 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 static IList<MetadataReference> CreateReferenceList(TestWorkspace workspace, XElement element)
{
var references = CreateCommonReferences(workspace, 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(workspace, 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(
ImmutableArray<DiagnosticAnalyzer>.Empty,
display: (string)analyzer.Attribute(AnalyzerDisplayAttributeName),
fullPath: (string)analyzer.Attribute(AnalyzerFullPathAttributeName)));
}
return analyzers;
}
private static IList<MetadataReference> CreateCommonReferences(TestWorkspace workspace, XElement element)
{
var references = new List<MetadataReference>();
var net45 = element.Attribute(CommonReferencesNet45AttributeName);
if (net45 != null &&
((bool?)net45).HasValue &&
((bool?)net45).Value)
{
references = new List<MetadataReference> { 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(workspace, 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 = new List<MetadataReference> { TestBase.MscorlibRef_v46, TestBase.SystemRef_v46, TestBase.SystemCoreRef_v46, TestBase.ValueTupleRef, TestBase.SystemRuntimeFacadeRef };
if (GetLanguage(workspace, 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 = new List<MetadataReference> { 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(workspace, 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.ToList();
}
var netstandard20 = element.Attribute(CommonReferencesNetStandard20Name);
if (netstandard20 != null &&
((bool?)netstandard20).HasValue &&
((bool?)netstandard20).Value)
{
references = TargetFrameworkUtil.NetStandard20References.ToList();
}
var net6 = element.Attribute(CommonReferencesNet6Name);
if (net6 != null &&
((bool?)net6).HasValue &&
((bool?)net6).Value)
{
references = TargetFrameworkUtil.GetReferences(TargetFramework.Net60).ToList();
}
var net7 = element.Attribute(CommonReferencesNet7Name);
if (net7 != null &&
((bool?)net7).HasValue &&
((bool?)net7).Value)
{
references = TargetFrameworkUtil.GetReferences(TargetFramework.Net70).ToList();
}
var mincorlib = element.Attribute(CommonReferencesMinCorlibName);
if (mincorlib != null &&
((bool?)mincorlib).HasValue &&
((bool?)mincorlib).Value)
{
references = new List<MetadataReference> { 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.");
}
}
}
}
}
|