src/AutoRest.CSharp/Common/AutoRest/Plugins/GeneratedCodeWorkspace.cs (241 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; using System.ClientModel; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using AutoRest.CSharp.Common.AutoRest.Plugins; using AutoRest.CSharp.Common.Input; using AutoRest.CSharp.Common.Output.PostProcessing; using Azure; using Azure.Core; using Azure.ResourceManager; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; namespace AutoRest.CSharp.AutoRest.Plugins { internal class GeneratedCodeWorkspace { public static readonly string SharedFolder = "shared"; public static readonly string GeneratedFolder = "Generated"; public static readonly string GeneratedTestFolder = "GeneratedTests"; private static readonly IReadOnlyList<MetadataReference> AssemblyMetadataReferences; private static readonly CSharpSyntaxRewriter SA1505Rewriter = new SA1505Rewriter(); static GeneratedCodeWorkspace() { var references = new List<MetadataReference> { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(Response).Assembly.Location), MetadataReference.CreateFromFile(typeof(ClientResult).Assembly.Location), MetadataReference.CreateFromFile(typeof(ArmResource).Assembly.Location), }; var trustedAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? "").Split(Path.PathSeparator); foreach (var tpl in trustedAssemblies) { references.Add(MetadataReference.CreateFromFile(tpl)); } AssemblyMetadataReferences = references; } private static readonly string[] SharedFolders = { SharedFolder }; private static readonly string[] GeneratedFolders = { GeneratedFolder }; private static readonly string[] GeneratedTestFolders = { GeneratedFolder, GeneratedTestFolder }; private static Task<Project>? _cachedProject; private Project _project; private Dictionary<string, XmlDocumentFile> _xmlDocFiles { get; } private Dictionary<string, string> _plainFiles { get; } private GeneratedCodeWorkspace(Project generatedCodeProject) { _project = generatedCodeProject; _xmlDocFiles = new(); _plainFiles = new(); } /// <summary> /// Creating AdHoc workspace and project takes a while, we'd like to preload this work /// to the generator startup time /// </summary> public static void Initialize() { _cachedProject = Task.Run(CreateGeneratedCodeProject); } public void AddGeneratedFile(string name, string text) => AddGeneratedFile(name, text, GeneratedFolders); public void AddGeneratedTestFile(string name, string text) => AddGeneratedFile(name, text, GeneratedTestFolders); private void AddGeneratedFile(string name, string text, string[] folders) { var document = _project.AddDocument(name, text, folders); var root = document.GetSyntaxRootAsync().GetAwaiter().GetResult(); Debug.Assert(root != null); root = root.WithAdditionalAnnotations(Simplifier.Annotation); document = document.WithSyntaxRoot(root); _project = document.Project; } /// <summary> /// Add generated doc file. /// </summary> /// <param name="name">Name of the doc file, including the relative path to the "Generated" folder.</param> /// <param name="xmlDocument">Content of the doc file.</param> public void AddGeneratedDocFile(string name, XmlDocumentFile xmlDocument) { _xmlDocFiles.Add(name, xmlDocument); } public void AddPlainFiles(string name, string content) { _plainFiles.Add(name, content); } public async IAsyncEnumerable<(string Name, string Text)> GetGeneratedFilesAsync() { var compilation = await _project.GetCompilationAsync(); Debug.Assert(compilation != null); var suppressedTypeNames = GetSuppressedTypeNames(compilation); List<Task<Document>> documents = new List<Task<Document>>(); foreach (Document document in _project.Documents) { // Skip writing shared files or originals if (!IsGeneratedDocument(document)) { continue; } documents.Add(ProcessDocument(compilation, document, suppressedTypeNames)); } var docs = await Task.WhenAll(documents); var needProcessGeneratedDocs = _xmlDocFiles.Any(); var generatedDocs = new Dictionary<string, SyntaxTree>(); foreach (var doc in docs) { var processed = doc; var text = await processed.GetSyntaxTreeAsync(); yield return (processed.Name, text!.ToString()); if (needProcessGeneratedDocs) // TODO -- this is a workaround. In HLC, in some cases, there are multiple documents with the same name added in this list, and we get "dictionary same key has been added" exception generatedDocs.Add(processed.Name, text); } foreach (var (docName, doc) in _xmlDocFiles) { var xmlWriter = doc.XmlDocWriter; if (generatedDocs.TryGetValue(doc.TestFileName, out var testDocument)) { var content = await XmlFormatter.FormatAsync(xmlWriter, testDocument); yield return (docName, content); } } foreach (var (file, content) in _plainFiles) { yield return (file, content); } } private async Task<Document> ProcessDocument(Compilation compilation, Document document, ImmutableHashSet<string> suppressedTypeNames) { var syntaxTree = await document.GetSyntaxTreeAsync(); if (syntaxTree != null) { var semanticModel = compilation.GetSemanticModel(syntaxTree); var modelRemoveRewriter = new MemberRemoverRewriter(_project, semanticModel, suppressedTypeNames); document = document.WithSyntaxRoot(SA1505Rewriter.Visit(modelRemoveRewriter.Visit(await syntaxTree.GetRootAsync()))); } document = await Simplifier.ReduceAsync(document); document = await Formatter.FormatAsync(document); return document; } internal static ImmutableHashSet<string> GetSuppressedTypeNames(Compilation compilation) { var suppressTypeAttribute = compilation.GetTypeByMetadataName(typeof(CodeGenSuppressTypeAttribute).FullName!)!; return compilation.Assembly.GetAttributes() .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, suppressTypeAttribute)) .Select(a => a.ConstructorArguments[0].Value) .OfType<string>() .ToImmutableHashSet(); } /// <summary> /// Add some additional files into this project /// </summary> /// <param name="directory"></param> /// <param name="skipPredicate"></param> /// <param name="folders"></param> public void AddDirectory(string directory, Func<string, bool>? skipPredicate = null, IEnumerable<string>? folders = null) { _project = AddDirectory(_project, directory, skipPredicate, folders); } /// <summary> /// Add the files in the directory to a project per a given predicate with the folders specified /// </summary> /// <param name="project"></param> /// <param name="directory"></param> /// <param name="skipPredicate"></param> /// <param name="folders"></param> /// <returns></returns> internal static Project AddDirectory(Project project, string directory, Func<string, bool>? skipPredicate = null, IEnumerable<string>? folders = null) { foreach (string sourceFile in Directory.GetFiles(directory, "*.cs", SearchOption.AllDirectories)) { if (skipPredicate != null && skipPredicate(sourceFile)) continue; project = project.AddDocument(sourceFile, File.ReadAllText(sourceFile), folders ?? Array.Empty<string>(), sourceFile).Project; } return project; } public static async Task<GeneratedCodeWorkspace> Create(string projectDirectory, string outputDirectory, string[] sharedSourceFolders) { var projectTask = Interlocked.Exchange(ref _cachedProject, null); var generatedCodeProject = projectTask != null ? await projectTask : CreateGeneratedCodeProject(); if (Path.IsPathRooted(projectDirectory) && Path.IsPathRooted(outputDirectory)) { projectDirectory = Path.GetFullPath(projectDirectory); outputDirectory = Path.GetFullPath(outputDirectory); generatedCodeProject = AddDirectory(generatedCodeProject, projectDirectory, skipPredicate: sourceFile => sourceFile.StartsWith(outputDirectory)); } foreach (var sharedSourceFolder in sharedSourceFolders) { generatedCodeProject = AddDirectory(generatedCodeProject, sharedSourceFolder, folders: SharedFolders); } generatedCodeProject = generatedCodeProject.WithParseOptions(new CSharpParseOptions(preprocessorSymbols: new[] { "EXPERIMENTAL" })); return new GeneratedCodeWorkspace(generatedCodeProject); } // TODO: Currently the outputDirectory is expected to be generated folder. We will handle the customization folder if there is a case. public static GeneratedCodeWorkspace CreateExistingCodeProject(string outputDirectory) { var workspace = new AdhocWorkspace(); Project project = workspace.AddProject("ExistingCode", LanguageNames.CSharp); if (Path.IsPathRooted(outputDirectory)) { outputDirectory = Path.GetFullPath(outputDirectory); project = AddDirectory(project, outputDirectory, null); } project = project .AddMetadataReferences(AssemblyMetadataReferences) .WithCompilationOptions(new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Disable)); return new GeneratedCodeWorkspace(project); } public static async Task<Compilation?> CreatePreviousContractFromDll(string xmlDocumentationpath, string dllPath) { var workspace = new AdhocWorkspace(); Project project = workspace.AddProject("PreviousContract", LanguageNames.CSharp); project = project .AddMetadataReferences(AssemblyMetadataReferences) .WithCompilationOptions(new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Disable)); project = project.AddMetadataReference(MetadataReference.CreateFromFile(dllPath, documentation: XmlDocumentationProvider.CreateFromFile(xmlDocumentationpath))); return await project.GetCompilationAsync(); } private static Project CreateGeneratedCodeProject() { var workspace = new AdhocWorkspace(); // TODO: This is not the right way to construct the workspace but it works Project generatedCodeProject = workspace.AddProject("GeneratedCode", LanguageNames.CSharp); generatedCodeProject = generatedCodeProject .AddMetadataReferences(AssemblyMetadataReferences) .WithCompilationOptions(new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Disable)); return generatedCodeProject; } public async Task<CSharpCompilation> GetCompilationAsync() { var compilation = await _project.GetCompilationAsync() as CSharpCompilation; Debug.Assert(compilation != null); return compilation; } public static bool IsCustomDocument(Document document) => !IsGeneratedDocument(document) && !IsSharedDocument(document); public static bool IsSharedDocument(Document document) => document.Folders.Contains(SharedFolder); public static bool IsGeneratedDocument(Document document) => document.Folders.Contains(GeneratedFolder); public static bool IsGeneratedTestDocument(Document document) => document.Folders.Contains(GeneratedTestFolder); /// <summary> /// This method delegates the caller to do something on the generated code project /// </summary> /// <param name="processor"></param> /// <returns></returns> public async Task PostProcess(Func<Project, Task<Project>> processor) { _project = await processor(_project); } /// <summary> /// This method invokes the postProcessor to do some post processing work /// Depending on the configuration, it will either remove + internalize, just internalize or do nothing /// </summary> /// <param name="postProcessor"></param> /// <returns></returns> public async Task PostProcessAsync(PostProcessor? postProcessor = null) { postProcessor ??= new PostProcessor(ImmutableHashSet<string>.Empty); switch (Configuration.UnreferencedTypesHandling) { case Configuration.UnreferencedTypesHandlingOption.KeepAll: break; case Configuration.UnreferencedTypesHandlingOption.Internalize: _project = await postProcessor.InternalizeAsync(_project); break; case Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize: _project = await postProcessor.InternalizeAsync(_project); _project = await postProcessor.RemoveAsync(_project); break; } } } }