|
// 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.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.DecompiledSource;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.UnitTests.MetadataAsSource;
public abstract partial class AbstractMetadataAsSourceTests
{
public const string DefaultMetadataSource = "public class C {}";
public const string DefaultSymbolMetadataName = "C";
internal class TestContext : IDisposable
{
public readonly TestWorkspace Workspace;
private readonly IMetadataAsSourceFileService _metadataAsSourceService;
public static TestContext Create(
string? projectLanguage = null,
IEnumerable<string>? metadataSources = null,
bool includeXmlDocComments = false,
string? sourceWithSymbolReference = null,
string? languageVersion = null,
string? metadataLanguageVersion = null,
string? metadataCommonReferences = null,
bool fileScopedNamespaces = false)
{
projectLanguage ??= LanguageNames.CSharp;
metadataSources ??= [];
metadataSources = !metadataSources.Any()
? new[] { AbstractMetadataAsSourceTests.DefaultMetadataSource }
: metadataSources;
var workspace = CreateWorkspace(
projectLanguage, metadataSources, includeXmlDocComments,
sourceWithSymbolReference, languageVersion, metadataLanguageVersion, metadataCommonReferences);
if (fileScopedNamespaces)
{
workspace.SetAnalyzerFallbackOptions(new OptionsCollection(LanguageNames.CSharp)
{
{ CSharpCodeStyleOptions.NamespaceDeclarations, new CodeStyleOption2<NamespaceDeclarationPreference>(NamespaceDeclarationPreference.FileScoped, NotificationOption2.Silent) }
});
}
return new TestContext(workspace);
}
public TestContext(TestWorkspace workspace)
{
Workspace = workspace;
_metadataAsSourceService = Workspace.GetService<IMetadataAsSourceFileService>();
}
public Solution CurrentSolution
{
get { return Workspace.CurrentSolution; }
}
public Project DefaultProject
{
get { return this.CurrentSolution.Projects.First(); }
}
public Task<MetadataAsSourceFile> GenerateSourceAsync(ISymbol symbol, Project? project = null, bool signaturesOnly = true)
{
project ??= this.DefaultProject;
Contract.ThrowIfNull(symbol);
// Generate and hold onto the result so it can be disposed of with this context
return _metadataAsSourceService.GetGeneratedFileAsync(Workspace, project, symbol, signaturesOnly, MetadataAsSourceOptions.Default, CancellationToken.None);
}
public async Task<MetadataAsSourceFile> GenerateSourceAsync(
string? symbolMetadataName = null,
Project? project = null,
bool signaturesOnly = true)
{
symbolMetadataName ??= AbstractMetadataAsSourceTests.DefaultSymbolMetadataName;
project ??= this.DefaultProject;
// Get an ISymbol corresponding to the metadata name
var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);
var diagnostics = compilation.GetDiagnostics().ToArray();
Assert.Equal(0, diagnostics.Length);
var symbol = await ResolveSymbolAsync(symbolMetadataName, compilation);
Contract.ThrowIfNull(symbol);
if (!signaturesOnly)
{
foreach (var reference in compilation.References)
{
if (AssemblyResolver.TestAccessor.ContainsInMemoryImage(reference))
{
continue;
}
if (reference is PortableExecutableReference portableExecutable)
{
Assert.True(File.Exists(portableExecutable.FilePath), $"'{portableExecutable.FilePath}' does not exist for reference '{portableExecutable.Display}'");
Assert.True(Path.IsPathRooted(portableExecutable.FilePath), $"'{portableExecutable.FilePath}' is not a fully-qualified file name");
}
else
{
Assert.True(File.Exists(reference.Display), $"'{reference.Display}' does not exist");
Assert.True(Path.IsPathRooted(reference.Display), $"'{reference.Display}' is not a fully-qualified file name");
}
}
}
// Generate and hold onto the result so it can be disposed of with this context
var result = await _metadataAsSourceService.GetGeneratedFileAsync(Workspace, project, symbol, signaturesOnly, MetadataAsSourceOptions.Default, CancellationToken.None);
return result;
}
public static void VerifyResult(MetadataAsSourceFile file, string expected)
{
var actual = File.ReadAllText(file.FilePath).Trim();
var actualSpan = file.IdentifierLocation.SourceSpan;
// Compare exact texts and verify that the location returned is exactly that
// indicated by expected
MarkupTestFile.GetSpan(expected, out expected, out var expectedSpan);
AssertEx.EqualOrDiff(expected, actual);
Assert.Equal(expectedSpan.Start, actualSpan.Start);
Assert.Equal(expectedSpan.End, actualSpan.End);
}
public async Task GenerateAndVerifySourceAsync(string symbolMetadataName, string expected, Project? project = null, bool signaturesOnly = true)
{
var result = await GenerateSourceAsync(symbolMetadataName, project, signaturesOnly);
VerifyResult(result, expected);
}
public static void VerifyDocumentReused(MetadataAsSourceFile a, MetadataAsSourceFile b)
=> Assert.Same(a.FilePath, b.FilePath);
public static void VerifyDocumentNotReused(MetadataAsSourceFile a, MetadataAsSourceFile b)
=> Assert.NotSame(a.FilePath, b.FilePath);
public void Dispose()
{
Workspace.Dispose();
}
public async Task<ISymbol?> ResolveSymbolAsync(string symbolMetadataName, Compilation? compilation = null)
{
if (compilation == null)
{
compilation = await this.DefaultProject.GetRequiredCompilationAsync(CancellationToken.None);
var diagnostics = compilation.GetDiagnostics().ToArray();
Assert.Equal(0, diagnostics.Length);
}
foreach (var reference in compilation.References)
{
var assemblySymbol = (IAssemblySymbol?)compilation.GetAssemblyOrModuleSymbol(reference);
Contract.ThrowIfNull(assemblySymbol);
var namedTypeSymbol = assemblySymbol.GetTypeByMetadataName(symbolMetadataName);
if (namedTypeSymbol != null)
{
return namedTypeSymbol;
}
else
{
// The symbol name could possibly be referring to the member of a named
// type. Parse the member symbol name.
var lastDotIndex = symbolMetadataName.LastIndexOf('.');
if (lastDotIndex < 0)
{
// The symbol name is not a member name and the named type was not found
// in this assembly
continue;
}
// The member symbol name itself could contain a dot (e.g. '.ctor'), so make
// sure we don't cut that off
while (lastDotIndex > 0 && symbolMetadataName[lastDotIndex - 1] == '.')
{
--lastDotIndex;
}
var memberSymbolName = symbolMetadataName[(lastDotIndex + 1)..];
var namedTypeName = symbolMetadataName[..lastDotIndex];
namedTypeSymbol = assemblySymbol.GetTypeByMetadataName(namedTypeName);
if (namedTypeSymbol != null)
{
var memberSymbol = namedTypeSymbol.GetMembers()
.Where(member => member.MetadataName == memberSymbolName)
.FirstOrDefault();
if (memberSymbol != null)
{
return memberSymbol;
}
}
}
}
return null;
}
private static bool ContainsVisualBasicKeywords(string input)
{
return
input.Contains("Class") ||
input.Contains("Structure") ||
input.Contains("Namespace") ||
input.Contains("Sub") ||
input.Contains("Function") ||
input.Contains("Dim");
}
private static string DeduceLanguageString(string input)
{
return ContainsVisualBasicKeywords(input)
? LanguageNames.VisualBasic : LanguageNames.CSharp;
}
private static TestWorkspace CreateWorkspace(
string projectLanguage,
IEnumerable<string>? metadataSources,
bool includeXmlDocComments,
string? sourceWithSymbolReference,
string? languageVersion,
string? metadataLanguageVersion,
string? metadataCommonReferences)
{
var languageVersionAttribute = languageVersion is null ? "" : $@" LanguageVersion=""{languageVersion}""";
var xmlString = string.Concat(@"
<Workspace>
<Project Language=""", projectLanguage, @""" CommonReferences=""true"" ReferencesOnDisk=""true""", languageVersionAttribute);
xmlString += ">";
metadataSources ??= new[] { AbstractMetadataAsSourceTests.DefaultMetadataSource };
foreach (var source in metadataSources)
{
var commonReferencesAttributeName = metadataCommonReferences ?? "CommonReferences";
var metadataLanguage = DeduceLanguageString(source);
var metadataLanguageVersionAttribute = metadataLanguageVersion is null ? "" : $@" LanguageVersion=""{metadataLanguageVersion}""";
xmlString = string.Concat(xmlString, $@"
<MetadataReferenceFromSource Language=""{metadataLanguage}"" {commonReferencesAttributeName}= ""true"" {metadataLanguageVersionAttribute} IncludeXmlDocComments=""{includeXmlDocComments}"">
<Document FilePath=""MetadataDocument"">
{SecurityElement.Escape(source)}
</Document>
</MetadataReferenceFromSource>");
}
if (sourceWithSymbolReference != null)
{
xmlString = string.Concat(xmlString, string.Format(@"
<Document FilePath=""SourceDocument"">
{0}
</Document>",
sourceWithSymbolReference));
}
xmlString = string.Concat(xmlString, @"
</Project>
</Workspace>");
// We construct our own composition here because we only want the decompilation metadata as source provider
// to be available.
var composition = EditorTestCompositions.EditorFeatures
.WithExcludedPartTypes(ImmutableHashSet.Create(typeof(IMetadataAsSourceFileProvider)))
.AddParts(typeof(DecompilationMetadataAsSourceFileProvider));
return TestWorkspace.Create(xmlString, composition: composition);
}
internal Document GetDocument(MetadataAsSourceFile file)
{
using var reader = File.OpenRead(file.FilePath);
var stringText = EncodedStringText.Create(reader);
Assert.True(_metadataAsSourceService.TryAddDocumentToWorkspace(file.FilePath, stringText.Container, out var _));
return stringText.Container.GetRelatedDocuments().Single();
}
internal async Task<ISymbol> GetNavigationSymbolAsync()
{
var testDocument = Workspace.Documents.Single(d => d.FilePath == "SourceDocument");
var document = Workspace.CurrentSolution.GetRequiredDocument(testDocument.Id);
var syntaxRoot = await document.GetRequiredSyntaxRootAsync(CancellationToken.None);
var semanticModel = await document.GetRequiredSemanticModelAsync(CancellationToken.None);
var symbol = semanticModel.GetSymbolInfo(syntaxRoot.FindNode(testDocument.SelectedSpans.Single())).Symbol;
Contract.ThrowIfNull(symbol);
return symbol;
}
}
}
|