private static IEnumerable <ICodeGenerator> FindCodeGenerators(GeneratorKnownTypes generatorKnownTypes, ImmutableArray <AttributeData> nodeAttributes, Func <AssemblyName, Assembly> assemblyLoader) { var foundGenerators = new List <(AttributeData attributeData, Type generatorType, int executionOrder)>(); foreach (var attributeData in nodeAttributes) { (Type generatorType, int executionOrder) = GetCodeGeneratorTypeForAttribute(generatorKnownTypes, attributeData, assemblyLoader); if (generatorType != null) { Logger.Info($"Found attribute = {attributeData.AttributeClass.Name}, type = {generatorType}, executionOrder = {executionOrder}"); foundGenerators.Add((attributeData, generatorType, executionOrder)); } } /* * if (foundGenerators.Count != foundGenerators.Select(g => g.executionOrder).Distinct().Count()) * { * throw new Exception("Unknown execution order - same order use more that once"); * } */ foreach (var foundGenerator in foundGenerators.OrderBy(g => g.executionOrder)) { ICodeGenerator generator; try { generator = (ICodeGenerator)Activator.CreateInstance(foundGenerator.generatorType, foundGenerator.attributeData); } catch (MissingMethodException) { throw new InvalidOperationException( $"Failed to instantiate {foundGenerator.generatorType}. ICodeGenerator implementations must have" + $" a constructor accepting Microsoft.CodeAnalysis.AttributeData argument."); } yield return(generator); } }
/// <summary> /// Runs the code generation as configured using this instance's properties. /// </summary> /// <param name="progress">Optional handler of diagnostics provided by code generator.</param> /// <param name="cancellationToken">Cancellation token to interrupt async operations.</param> public void Generate(IProgress <Diagnostic> progress = null, CancellationToken cancellationToken = default) { Verify.Operation(this.Compile != null, $"{nameof(Compile)} must be set first."); Verify.Operation(this.ReferencePath != null, $"{nameof(ReferencePath)} must be set first."); Verify.Operation(this.IntermediateOutputDirectory != null, $"{nameof(IntermediateOutputDirectory)} must be set first."); Verify.Operation(this.GeneratorAssemblySearchPaths != null, $"{nameof(GeneratorAssemblySearchPaths)} must be set first."); var compilation = this.CreateCompilation(cancellationToken); var generatorKnowTypes = new GeneratorKnownTypes(compilation); string generatorAssemblyInputsFile = Path.Combine(this.IntermediateOutputDirectory, InputAssembliesIntermediateOutputFileName); // For incremental build, we want to consider the input->output files as well as the assemblies involved in code generation. DateTime assembliesLastModified = GetLastModifiedAssemblyTime(generatorAssemblyInputsFile); var fileFailures = new List <Exception>(); using (var hasher = System.Security.Cryptography.SHA1.Create()) { foreach (var inputSyntaxTree in compilation.SyntaxTrees) { cancellationToken.ThrowIfCancellationRequested(); string sourceHash = Convert.ToBase64String(hasher.ComputeHash(Encoding.UTF8.GetBytes(inputSyntaxTree.FilePath)), 0, 6).Replace('/', '-'); Logger.Info($"File \"{inputSyntaxTree.FilePath}\" hashed to {sourceHash}"); string outputFilePath = Path.Combine(this.IntermediateOutputDirectory, Path.GetFileNameWithoutExtension(inputSyntaxTree.FilePath) + $".{sourceHash}.generated.cs"); string replacedOutputFilePath = Path.Combine(this.IntermediateOutputDirectory, Path.GetFileNameWithoutExtension(inputSyntaxTree.FilePath) + $".{sourceHash}_r.generated.cs"); var fileRegenerated = false; var outputFilePathExists = File.Exists(outputFilePath); var replacedOutputFilePathExists = !outputFilePathExists && File.Exists(replacedOutputFilePath); // Code generation is relatively fast, but it's not free. // So skip files that haven't changed since we last generated them. DateTime outputLastModified = outputFilePathExists ? File.GetLastWriteTime(outputFilePath) : replacedOutputFilePathExists ? File.GetLastWriteTime(replacedOutputFilePath) : DateTime.MinValue; if (File.GetLastWriteTime(inputSyntaxTree.FilePath) > outputLastModified || assembliesLastModified > outputLastModified) { fileRegenerated = true; int retriesLeft = 3; do { try { (var generatedSyntaxTree, var replaceFile) = DocumentTransform.TransformAsync( compilation, generatorKnowTypes, inputSyntaxTree, this.ProjectDirectory, this.LoadAssembly, progress).GetAwaiter().GetResult(); if (replaceFile) { outputFilePath = replacedOutputFilePath; } var outputText = generatedSyntaxTree.GetText(cancellationToken); using (var outputFileStream = File.OpenWrite(outputFilePath)) using (var outputWriter = new StreamWriter(outputFileStream)) { outputText.Write(outputWriter); // Truncate any data that may be beyond this point if the file existed previously. outputWriter.Flush(); outputFileStream.SetLength(outputFileStream.Position); } bool anyTypesGenerated = generatedSyntaxTree?.GetRoot(cancellationToken).DescendantNodes().OfType <TypeDeclarationSyntax>().Any() ?? false; if (!anyTypesGenerated) { this.emptyGeneratedFiles.Add(outputFilePath); } break; } catch (IOException ex) when(ex.HResult == ProcessCannotAccessFileHR && retriesLeft > 0) { retriesLeft--; Task.Delay(200).Wait(); } catch (Exception ex) { ReportError(progress, "CGR001", inputSyntaxTree, ex); fileFailures.Add(ex); break; } }while (true); } if (!fileRegenerated && replacedOutputFilePathExists) { outputFilePath = replacedOutputFilePath; } this.generatedFiles.Add(outputFilePath); if (outputFilePath == replacedOutputFilePath) { replacedFiles.Add(inputSyntaxTree.FilePath); } } } this.SaveGeneratorAssemblyList(generatorAssemblyInputsFile); if (fileFailures.Count > 0) { throw new AggregateException(fileFailures); } }
".Replace("\r\n", "\n").Replace("\n", Environment.NewLine); // normalize regardless of git checkout policy /// <summary> /// Produces a new document in response to any code generation attributes found in the specified document. /// </summary> /// <param name="csCompilation">The compilation to which the document belongs.</param> /// <param name="inputDocument">The document to scan for generator attributes.</param> /// <param name="projectDirectory">The path of the <c>.csproj</c> project file.</param> /// <param name="assemblyLoader">A function that can load an assembly with the given name.</param> /// <param name="progress">Reports warnings and errors in code generation.</param> /// <returns>A task whose result is the generated document.</returns> public static async Task <(SyntaxTree, bool)> TransformAsync( CSharpCompilation csCompilation, GeneratorKnownTypes generatorKnownTypes, SyntaxTree inputDocument, string projectDirectory, Func <AssemblyName, Assembly> assemblyLoader, IProgress <Diagnostic> progress) { Requires.NotNull(csCompilation, nameof(csCompilation)); Requires.NotNull(inputDocument, nameof(inputDocument)); Requires.NotNull(assemblyLoader, nameof(assemblyLoader)); var inputSemanticModel = csCompilation.GetSemanticModel(inputDocument); var inputCompilationUnit = inputDocument.GetCompilationUnitRoot(); var emittedExterns = inputCompilationUnit .Externs .Select(x => x.WithoutTrivia()) .ToImmutableArray(); var emittedUsings = inputCompilationUnit .Usings .Select(x => x.WithoutTrivia()) .ToImmutableArray(); async Task <(CSharpCompilation compilation, SemanticModel semanticModel, SyntaxNode resRootNode, SyntaxNode resRootNodeTracked, bool treeChanged, RichGenerationResult richGenerationResult)> ProcessNodeTransformation( CSharpCompilation compilation, SemanticModel semanticModel, SyntaxNode rootNode, SyntaxNode rootNodeTracked, bool treeChanged, SyntaxNode workNode) { (SyntaxNode, SyntaxNode) ProcessNodes(SyntaxNode root, SyntaxNode processingNode, List <ChangeMember> childs) { Dictionary <SyntaxNode, SyntaxNode> GetFilteredNodes(List <ChangeMember> innerChilds, Func <ChangeMember, bool> filterFunc, Func <ChangeMember, (SyntaxNode, SyntaxNode)> transformFunc) { return(innerChilds.Where(filterFunc).Select(transformFunc).ToDictionary(p => p.Item1, p => p.Item2)); } SyntaxNode newProcessingNode = null; SyntaxNode newRemovedNode = null; var replaceNodes = GetFilteredNodes(childs, m => m.OldMember != null && m.NewMember != null, c => (root.GetCurrentNode(c.OldMember), c.NewMember.TrackNodes(c.NewMember.DescendantNodesAndSelf()))); if (replaceNodes.Any()) { newProcessingNode = childs.FirstOrDefault(m => m.OldMember != null && m.NewMember != null && m.OldMember == processingNode)?.NewMember; root = root.ReplaceNodes(replaceNodes.Keys, (o, r) => replaceNodes[o]); } var removeNodes = GetFilteredNodes(childs, m => m.OldMember != null && m.NewMember == null, c => (root.GetCurrentNode(c.OldMember), null)); if (removeNodes.Any()) { newRemovedNode = childs.FirstOrDefault(m => m.OldMember != null && m.NewMember == null)?.OldMember; root = root.RemoveNodes(removeNodes.Keys, SyntaxRemoveOptions.KeepNoTrivia); } var insertNodes = GetFilteredNodes(childs, m => m.OldMember == null && m.NewMember != null, c => (c.NewMember.TrackNodes(c.NewMember.DescendantNodesAndSelf()), c.BaseMember)); foreach (var addNode in insertNodes.Keys) { var baseNode = insertNodes[addNode]; if (baseNode == null) { baseNode = root.ChildNodes().OfType <MemberDeclarationSyntax>().Last(); } else { baseNode = root.GetCurrentNode(baseNode); } Logger.Info($"BaseNode type = {baseNode?.GetType()}"); root = root.InsertNodesAfter(baseNode, new[] { addNode }); } if (newProcessingNode != null) { newProcessingNode = root.GetCurrentNode(newProcessingNode); } else { newProcessingNode = newRemovedNode != null ? null : processingNode; } return(root, newProcessingNode); } if (workNode == null) { return(null, null, null, null, false, null); } var richGeneratorResult = new RichGenerationResult(); var newMemberNode = workNode; if (workNode is CompilationUnitSyntax || workNode is NamespaceDeclarationSyntax || workNode is TypeDeclarationSyntax) { var rootNodeIsType = workNode is TypeDeclarationSyntax; foreach (var node in workNode.ChildNodes() .Where(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax || n is TypeDeclarationSyntax || rootNodeIsType) .OfType <CSharpSyntaxNode>().ToList()) { var processedNode = node as SyntaxNode; var innerNodesReplacement = await ProcessNodeTransformation(compilation, semanticModel, rootNode, rootNodeTracked, treeChanged, node); compilation = innerNodesReplacement.compilation; semanticModel = innerNodesReplacement.semanticModel; rootNode = innerNodesReplacement.resRootNode; rootNodeTracked = innerNodesReplacement.resRootNodeTracked; treeChanged = innerNodesReplacement.treeChanged; richGeneratorResult.Externs = richGeneratorResult.Externs.AddRange(innerNodesReplacement.richGenerationResult.Externs); richGeneratorResult.Usings = richGeneratorResult.Usings.AddRange(innerNodesReplacement.richGenerationResult.Usings); richGeneratorResult.AttributeLists = richGeneratorResult.AttributeLists.AddRange(innerNodesReplacement.richGenerationResult.AttributeLists); richGeneratorResult.Members.AddRange(innerNodesReplacement.richGenerationResult.Members); } } newMemberNode = rootNode.GetCurrentNode(workNode) ?? workNode; var attributeData = GetAttributeData(compilation, semanticModel, newMemberNode); var generators = FindCodeGenerators(generatorKnownTypes, attributeData, assemblyLoader).ToList(); if (generators.Count > 0) { Logger.Info($"Generators founded = {generators.Count}"); } foreach (var generator in generators) { var context = new TransformationContext( newMemberNode, semanticModel, compilation, projectDirectory, emittedUsings, emittedExterns); var richGenerator = generator as IRichCodeGenerator ?? new EnrichingCodeGeneratorProxy(generator); var emitted = await richGenerator.GenerateRichAsync(context, progress, CancellationToken.None); Logger.Info($"Processed generator = {generator.GetType()}, node = {newMemberNode}, members count = {emitted.Members.Count}"); foreach (var member in emitted.Members) { Logger.Info($"member " + $"OldMember type = {member.OldMember?.GetType()}, OldMember value = {member.OldMember}" + $"BaseMember type = {member.BaseMember?.GetType()}, BaseMember value = {member.BaseMember}" + $"NewMember type = {member.NewMember?.GetType()}, NewMember value = {member.NewMember}"); } if (!treeChanged) { treeChanged = emitted.Members.Any(m => !(m.OldMember == null && m.BaseMember == null)); Logger.Info($"treeChanged={treeChanged}"); } var oldRootNode = rootNode; (rootNode, newMemberNode) = ProcessNodes(rootNodeTracked, newMemberNode, emitted.Members); rootNodeTracked = rootNode.TrackNodes(rootNode.DescendantNodesAndSelf()); compilation = compilation.ReplaceSyntaxTree(oldRootNode.SyntaxTree, rootNode.SyntaxTree); semanticModel = compilation.GetSemanticModel(rootNode.SyntaxTree); richGeneratorResult.Externs = richGeneratorResult.Externs.AddRange(emitted.Externs); richGeneratorResult.Usings = richGeneratorResult.Usings.AddRange(emitted.Usings); richGeneratorResult.AttributeLists = richGeneratorResult.AttributeLists.AddRange(emitted.AttributeLists); richGeneratorResult.Members.AddRange(emitted.Members.Where(m => m.OldMember == null && m.BaseMember == null)); // Check current node removed if (newMemberNode != null) { newMemberNode = rootNode.GetCurrentNode(newMemberNode) ?? newMemberNode; } else { break; } } return(compilation, semanticModel, rootNode, rootNodeTracked, treeChanged, richGeneratorResult); } var oldDocumentRootNode = inputDocument.GetRoot() as CSharpSyntaxNode; var documentRootNode = oldDocumentRootNode.TrackNodes(oldDocumentRootNode.DescendantNodesAndSelf()); var processNodeResult = await ProcessNodeTransformation(csCompilation, inputSemanticModel, oldDocumentRootNode, documentRootNode, false, oldDocumentRootNode); var emittedMembers = processNodeResult.treeChanged ? processNodeResult.resRootNode.ChildNodes().OfType <MemberDeclarationSyntax>().ToImmutableArray() : processNodeResult.richGenerationResult.Members.Where(m => m.NewMember != null).Select(m => m.NewMember).OfType <MemberDeclarationSyntax>().ToImmutableArray(); var compilationUnit = SyntaxFactory.CompilationUnit( SyntaxFactory.List(emittedExterns.AddRange(processNodeResult.richGenerationResult.Externs)), SyntaxFactory.List(emittedUsings.AddRange(processNodeResult.richGenerationResult.Usings)), SyntaxFactory.List(processNodeResult.richGenerationResult.AttributeLists), SyntaxFactory.List(emittedMembers)) .WithLeadingTrivia(SyntaxFactory.Comment(GeneratedByAToolPreamble)) .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed) .NormalizeWhitespace(); return(compilationUnit.SyntaxTree, processNodeResult.treeChanged); }
private static (Type type, int executionOrder) GetCodeGeneratorTypeForAttribute(GeneratorKnownTypes generatorKnownTypes, AttributeData attributeData, Func <AssemblyName, Assembly> assemblyLoader) { Requires.NotNull(assemblyLoader, nameof(assemblyLoader)); var attributeType = attributeData.AttributeClass; if (attributeType != null) { foreach (var generatorCandidateAttribute in attributeType.GetAttributes()) { if (generatorCandidateAttribute.AttributeClass.OriginalDefinition.Equals(generatorKnownTypes.CodeGenerationAttributeAttributeTypeSymbol)) { var executionOrder = 0; var hasOrderInterface = attributeType.AllInterfaces.Any(i => i.OriginalDefinition.Equals(generatorKnownTypes.IOrderCodeGenerationTypeSymbol)); if (hasOrderInterface) { var executionOrderArgument = attributeData.NamedArguments.FirstOrDefault(a => a.Key == nameof(IOrderedCodeGeneration.ExecutionOrder)); if (!string.IsNullOrEmpty(executionOrderArgument.Key)) { executionOrder = (int)executionOrderArgument.Value.Value; } } string assemblyName = null; string fullTypeName = null; TypedConstant firstArg = generatorCandidateAttribute.ConstructorArguments.Single(); if (firstArg.Value is string typeName) { // This string is the full name of the type, which MAY be assembly-qualified. int commaIndex = typeName.IndexOf(','); bool isAssemblyQualified = commaIndex >= 0; if (isAssemblyQualified) { fullTypeName = typeName.Substring(0, commaIndex); assemblyName = typeName.Substring(commaIndex + 1).Trim(); } else { fullTypeName = typeName; assemblyName = generatorCandidateAttribute.AttributeClass.ContainingAssembly.Name; } } else if (firstArg.Value is INamedTypeSymbol typeOfValue) { // This was a typeof(T) expression fullTypeName = GetFullTypeName(typeOfValue); assemblyName = typeOfValue.ContainingAssembly.Name; } if (assemblyName != null) { Logger.Info($"assemblyLoader assemblyName = {assemblyName}, fullTypeName = {fullTypeName}"); var assembly = assemblyLoader(new AssemblyName(assemblyName)); if (assembly != null) { return(assembly.GetType(fullTypeName), executionOrder); } } Verify.FailOperation("Unable to find code generator: {0} in {1}", fullTypeName, assemblyName); } } } return(null, 0); }