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);
            }
        }
Ejemplo n.º 2
0
        /// <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);
        }