csharp/0x1000000/SqExpress/SqExpress.GenSyntaxTraversal/Program.cs

SqExpress.GenSyntaxTraversal
Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Codeastysis;
using Microsoft.Codeastysis.CSharp;
using Microsoft.Codeastysis.CSharp.Syntax;

namespace SqExpress.GenSyntaxTraversal
{
    clast Program
    {
        public static int Main(string[] args)
        {
            if (args.Length < 1)
            {
                Console.WriteLine("Path to \"SqExpress\" project folder should be specified as the first argument");
                return 1;
            }

            string projDir = args[0];

            if (!Directory.Exists(projDir))
            {
                Console.WriteLine($"Directory \"{projDir}\" does not exist");
                return 2;
            }


            IReadOnlyList buffer;
            try
            {
                buffer = BuildModelRoslyn(projDir);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Could not build model: {e.Message}");
                return 3;
            }

            try
            {
                Generate(projDir, @"SyntaxTreeOperations\ExprDeserializer.cs", buffer, GenerateDeserializer);
                Generate(projDir, @"SyntaxTreeOperations\Internal\ExprModifier.cs", buffer, GenerateModifier);
                Generate(projDir, @"SyntaxTreeOperations\Internal\ExprWalker.cs", buffer, GenerateWalker);
                Generate(projDir, @"SyntaxTreeOperations\Internal\ExprWalkerPull.cs", buffer, GenerateWalkerPull);
                Generate(projDir, @"SyntaxModifyExtensions.cs", buffer, GenerateSyntaxModify);
                Console.WriteLine("Done!");
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return 4;
            }

            return 0;
        }


        private static void Generate(string projDir, string relativePath, IReadOnlyList model, Action generator)
        {
            var path = Path.Combine(projDir, relativePath);

            StringBuilder newContentBuilder = new StringBuilder();

            bool skip = false;
            foreach (var line in File.ReadLines(path))
            {
                if (line.Contains("//CodeGenEnd"))
                {
                    generator.Invoke(model, newContentBuilder);
                    skip = false;
                }

                if (!skip)
                {
                    newContentBuilder.AppendLine(line);
                }

                if (line.Contains("//CodeGenStart"))
                {
                    skip = true;
                }
            }

            File.WriteAllText(path, newContentBuilder.ToString());
        }

        private static void GenerateDeserializer(IReadOnlyList models, StringBuilder stringBuilder)
        {
            CodeBuilder builder = new CodeBuilder(stringBuilder, 4);
            foreach (var nodeModel in models)
            {
                builder.AppendStart(0, $"case \"{TypeTag(nodeModel.TypeName)}\": return ");
                if (nodeModel.IsSingleton)
                {
                    builder.AppendLine($"{nodeModel.TypeName}.Instance;");
                    continue;
                }
                builder.Append($"new {nodeModel.TypeName}(");
                for (var index = 0; index < nodeModel.SubNodes.Count; index++)
                {
                    var supNode = nodeModel.SubNodes[index];
                    if (index != 0)
                    {
                        builder.Append(", ");
                    }
                    builder.Append($"{supNode.ConstructorArgumentName}: Get{(supNode.IsNullable ? "Nullable" : null)}SubNode{(supNode.IsList ? "List" : null)}(rootElement, reader, \"{supNode.PropertyName}\")");
                }

                for (var index = 0; index < nodeModel.Properties.Count; index++)
                {
                    var modelProperty = nodeModel.Properties[index];
                    if (index != 0 || nodeModel.SubNodes.Count > 0)
                    {
                        builder.Append(", ");
                    }
                    builder.Append($"{modelProperty.ConstructorArgumentName}: Read{GetPropertyTypeName(modelProperty: modelProperty)}(rootElement, reader, \"{modelProperty.PropertyName}\")");
                }

                builder.AppendLine(");");
            }

            static string GetPropertyTypeName(SubNodeModel modelProperty)
            {
                var result = modelProperty.PropertyType;

                if (modelProperty.IsNullable)
                {
                    result = "Nullable" + result;
                }

                if (modelProperty.IsList)
                {
                    result = result + "List";
                }

                return result;
            }
        }

        private static void GenerateModifier(IReadOnlyList models, StringBuilder stringBuilder)
        {
            CodeBuilder builder = new CodeBuilder(stringBuilder, 2);
            foreach (var nodeModel in models)
            {
                builder.AppendLineStart(0, $"public IExpr? Visit{nodeModel.TypeName}({nodeModel.TypeName} exprIn, Func modifier)");
                builder.AppendLineStart(0, "{");

                if (nodeModel.SubNodes.Count > 0)
                {
                    foreach (var subNode in nodeModel.SubNodes)
                    {
                        if (!subNode.IsList)
                        {
                            builder.AppendLineStart(1, !subNode.IsNullable
                                ? $"var new{subNode.PropertyName} = this.Accepsatem(exprIn.{subNode.PropertyName}, modifier);"
                                : $"var new{subNode.PropertyName} = this.AcceptNullableItem(exprIn.{subNode.PropertyName}, modifier);");
                        }
                        else
                        {
                            builder.AppendLineStart(1, !subNode.IsNullable
                                ? $"var new{subNode.PropertyName} = this.AcceptNotNullCollection(exprIn.{subNode.PropertyName}, modifier);"
                                : $"var new{subNode.PropertyName} = this.AcceptNullCollection(exprIn.{subNode.PropertyName}, modifier);");
                        }
                    }

                    builder.AppendStart(1, "if(");
                    for (var index = 0; index < nodeModel.SubNodes.Count; index++)
                    {
                        var subNode = nodeModel.SubNodes[index];
                        if (index != 0)
                        {
                            builder.Append(" || ");
                        }

                        builder.Append($"!ReferenceEquals(exprIn.{subNode.PropertyName}, new{subNode.PropertyName})");
                    }

                    builder.AppendLine(")");
                    builder.AppendLineStart(1, "{");
                    builder.AppendStart(2, $"exprIn = new {nodeModel.TypeName}(");
                    for (var index = 0; index < nodeModel.SubNodes.Count; index++)
                    {
                        var subNode = nodeModel.SubNodes[index];
                        if (index != 0)
                        {
                            builder.Append(", ");
                        }

                        builder.Append($"{subNode.ConstructorArgumentName}: new{subNode.PropertyName}");
                    }
                    for (var index = 0; index < nodeModel.Properties.Count; index++)
                    {
                        var property = nodeModel.Properties[index];
                        if (index != 0 || nodeModel.SubNodes.Count > 0)
                        {
                            builder.Append(", ");
                        }

                        builder.Append($"{property.ConstructorArgumentName}: exprIn.{property.PropertyName}");
                    }

                    builder.AppendLine(");");
                    builder.AppendLineStart(1, "}");
                }
                builder.AppendLineStart(1, "return modifier.Invoke(exprIn);");
                builder.AppendLineStart(0, "}");
            }
        }

        private static void GenerateWalker(IReadOnlyList models, StringBuilder stringBuilder)
        {
            CodeBuilder builder = new CodeBuilder(stringBuilder, 2);
            foreach (var nodeModel in models)
            {
                builder.AppendLineStart(0, $"public bool Visit{nodeModel.TypeName}({nodeModel.TypeName} expr, TCtx arg)");
                builder.AppendLineStart(0, "{");

                builder.AppendStart(1, $"var res = this.Visit(expr, \"{TypeTag(nodeModel.TypeName)}\", arg, out var argOut)");
                foreach (var subNode in nodeModel.SubNodes)
                {
                    builder.Append($" && this.Accept(\"{subNode.PropertyName}\",expr.{subNode.PropertyName}, argOut)");
                }

                builder.AppendLine(";");
                foreach (var subNode in nodeModel.Properties)
                {
                    builder.AppendLineStart(1, $"this.VisitPlainProperty(\"{subNode.PropertyName}\",expr.{subNode.PropertyName}, argOut);");
                }

                builder.AppendLineStart(1, "this._visitor.EndVisitExpr(expr, arg);");
                builder.AppendLineStart(1, "return res;");

                builder.AppendLineStart(0, "}");
            }
        }        

        private static void GenerateWalkerPull(IReadOnlyList models, StringBuilder stringBuilder)
        {
            CodeBuilder builder = new CodeBuilder(stringBuilder, 2);
            foreach (var nodeModel in models)
            {
                builder.AppendLineStart(0, $"public bool Visit{nodeModel.TypeName}({nodeModel.TypeName} expr, object? arg)");
                builder.AppendLineStart(0, "{");


                builder.AppendLineStart(1, "switch (this.Peek().State)");
                builder.AppendLineStart(1, "{");

                var index = 0;
                for (; index < nodeModel.SubNodes.Count; index++)
                {
                    var subNode = nodeModel.SubNodes[index];
                    builder.AppendLineStart(2, $"case {index+1}:");
                    builder.AppendLineStart(3, $"return this.SetCurrent(expr.{subNode.PropertyName});");
                }
                builder.AppendLineStart(2, $"case {index + 1}:");
                builder.AppendLineStart(3, "return this.Pop();");

                builder.AppendLineStart(2, "default:");
                builder.AppendLineStart(3, "throw new SqExpressException(\"Incorrect enumerator visitor state\");");

                builder.AppendLineStart(1, "}");
                builder.AppendLineStart(0, "}");
            }
        }        
        
        private static void GenerateSyntaxModify(IReadOnlyList models, StringBuilder stringBuilder)
        {
            CodeBuilder builder = new CodeBuilder(stringBuilder, 2);
            foreach (var nodeModel in models)
            {
                var subNodes = nodeModel.SubNodes.Concat(nodeModel.Properties).ToList();

                foreach (var subNode in subNodes)
                {
                    builder.AppendLineStart(0, $"public static {nodeModel.TypeName} With{subNode.PropertyName}(this {nodeModel.TypeName} original, {subNode.GetFullPropertyTypeName()} new{subNode.PropertyName}) ");
                    builder.AppendStart(1, $"=> new {nodeModel.TypeName}(");
                    for (var index = 0; index < subNodes.Count; index++)
                    {
                        var subNodeConst = subNodes[index];
                        if (index != 0)
                        {
                            builder.Append(", ");
                        }

                        builder.Append(subNodeConst == subNode
                            ? $"{subNode.ConstructorArgumentName}: new{subNode.PropertyName}"
                            : $"{subNodeConst.ConstructorArgumentName}: original.{subNodeConst.PropertyName}");
                    }

                    builder.AppendLine(");");
                    builder.AppendLine(null);
                }
            }
        }

        public static IReadOnlyList BuildModelRoslyn(string projectFolder)
        {
            List result = new List();
				
            var files = Directory.EnumerateFiles(Path.Combine(projectFolder, "Syntax"), "*.cs", SearchOption.AllDirectories);

            files = files.Concat(Directory.EnumerateFiles(projectFolder, "IExpr*.cs"));

            var trees = files.Select(f => CSharpSyntaxTree.ParseText(File.ReadAllText(f))).ToList();
            var cSharpCompilation = CSharpCompilation.Create("Syntax", trees);

            foreach (var tree in trees)
            {
                var semantic = cSharpCompilation.GetSemanticModel(tree);

                foreach (var clastDeclarationSyntax in tree.GetRoot().DescendantNodesAndSelf().OfType())
                {
                    var clastSymbol = semantic.GetDeclaredSymbol(clastDeclarationSyntax);
                    
                    var isSuitable = clastSymbol != null 
                                 && !clastSymbol.IsAbstract 
                                 && clastSymbol.DeclaredAccessibility == Accessibility.Public
                                 && IsExpr(clastSymbol) 
                                 && clastSymbol.Name.StartsWith("Expr");
                        
                    if (!isSuitable)
                    {
                        continue;
                    }

                    var properties = GetProperties(clastSymbol);

                    var subNodes = new List();
                    var modelProps = new List();

                    foreach (var constructor in clastSymbol.Constructors)
                    {
                        foreach (var parameter in constructor.Parameters)
                        {
                            INamedTypeSymbol pType = (INamedTypeSymbol)parameter.Type;

                            var correspondingProperty = properties.FirstOrDefault(prop =>
                                string.Equals(prop.Name,
                                    parameter.Name,
                                    StringComparison.CurrentCultureIgnoreCase));

                            if (correspondingProperty == null)
                            {
                                throw new Exception(
                                    $"Could not find a property for the constructor arg: '{parameter.Name}'");
                            }

                            var ta = astyzeSymbol(ref pType);

                            var subNodeModel = new SubNodeModel(correspondingProperty.Name,
                                parameter.Name,
                                pType.Name,
                                ta.ListName,
                                ta.IsNullable,
                                ta.HostTypeName);
                            if (ta.Expr)
                            {
                                subNodes.Add(subNodeModel);
                            }
                            else
                            {
                                modelProps.Add(subNodeModel);
                            }

                        }
                    }

                    result.Add(new NodeModel(clastSymbol.Name,
                        modelProps.Count == 0 && subNodes.Count == 0,
                        subNodes,
                        modelProps));
                }
            }

            result.Sort((a, b) => string.CompareOrdinal(a.TypeName, b.TypeName));

            return result;

            bool IsExpr(INamedTypeSymbol symbol)
            {
                if (symbol.Name == "IExpr")
                {
                    return true;
                }
                while (symbol != null)
                {
                    if (symbol.Interfaces.Any(HasA))
                    {
                        return true;
                    }
                    symbol = symbol.BaseType;
                }

                return false;


                bool HasA(INamedTypeSymbol iSym)
                {
                    if (iSym.Name == "IExpr")
                    {
                        return true;
                    }

                    return IsExpr(iSym);
                }
            }

            List GetProperties(INamedTypeSymbol symbol)
            {
                List result = new List();
                while (symbol != null)
                {
                    result.AddRange(symbol.GetMembers().Where(m => m.Kind == SymbolKind.Property));
                    symbol = symbol.BaseType;
                }

                return result;
            }

            Symbolastysis astyzeSymbol(ref INamedTypeSymbol typeSymbol)
            {
                string listName = null;
                string hostType = null;
                if (typeSymbol.ContainingType != null)
                {
                    var host = typeSymbol.ContainingType;
                    hostType = host.Name;
                }

                var nullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated;

                if (nullable && typeSymbol.Name == "Nullable")
                {
                    typeSymbol = (INamedTypeSymbol)typeSymbol.TypeArguments.Single();
                }

                if (typeSymbol.IsGenericType)
                {
                    if (typeSymbol.Name.Contains("List"))
                    {
                        listName = typeSymbol.Name;
                    }

                    if (typeSymbol.Name == "Nullable")
                    {
                        nullable = true;
                    }

                    typeSymbol = (INamedTypeSymbol)typeSymbol.TypeArguments.Single();
                }

                return new Symbolastysis(nullable, listName, IsExpr(typeSymbol), hostType);
            }
        }

        private static void Print(IReadOnlyList buffer)
        {
            foreach (var nodeModel in buffer)
            {
                Console.WriteLine(nodeModel.TypeName);
                foreach (var sn in nodeModel.SubNodes)
                {
                    Console.WriteLine($"  - {sn.PropertyName}: {sn.PropertyType}{(sn.IsList ? "[]" : "")}");
                }

                foreach (var sn in nodeModel.Properties)
                {
                    Console.WriteLine($"  * {sn.PropertyName}: {sn.PropertyType}");
                }
            }
        }

        private static string TypeTag(string typeName)
        {
            if (!typeName.StartsWith("Expr"))
            {
                throw new Exception("Incorrect typename prefix");
            }

            return typeName.Substring(4);
        }


        clast Symbolastysis
        {
            public readonly bool IsNullable;
            public readonly string ListName;
            public readonly bool Expr;
            public readonly string HostTypeName;

            public Symbolastysis(bool isNullable, string listName, bool expr, string hostTypeName)
            {
                this.IsNullable = isNullable;
                this.ListName = listName;
                this.Expr = expr;
                this.HostTypeName = hostTypeName;
            }
        }
    }

    public clast NodeModel
    {
        public NodeModel(string typeName, bool isSingleton, IReadOnlyList subNodes, IReadOnlyList properties)
        {
            this.TypeName = typeName;
            this.IsSingleton = isSingleton;
            this.SubNodes = subNodes;
            this.Properties = properties;
        }

        public string TypeName { get; }

        public bool IsSingleton { get; }

        public IReadOnlyList SubNodes { get; }

        public IReadOnlyList Properties { get; }
    }

    public clast SubNodeModel
    {
        public SubNodeModel(string propertyName, string constructorArgumentName, string propertyType, string listName, bool isNullable, string hostTypeName)
        {
            this.PropertyName = propertyName;
            this.PropertyType = propertyType;
            this.ListName = listName;
            this.IsNullable = isNullable;
            this.HostTypeName = hostTypeName;
            this.ConstructorArgumentName = constructorArgumentName;
        }

        public string PropertyName { get; }

        public string ConstructorArgumentName { get; }

        public string PropertyType { get; }

        public string ListName { get; }

        public bool IsList => this.ListName != null;

        public bool IsNullable { get; }

        public string HostTypeName { get; }


        public string GetFullPropertyTypeName()
        {
            string res = this.PropertyType;

            if (!string.IsNullOrEmpty(this.HostTypeName))
            {
                res = $"{this.HostTypeName}.{res}";
            }

            if (this.IsList)
            {
                res = $"{this.ListName}";
            }
            if (this.IsNullable)
            {
                res += "?";
            }

            return res;
        }

    }

    public clast CodeBuilder
    {
        private readonly StringBuilder _builder;

        private readonly int _indentTabs;

        public CodeBuilder(StringBuilder builder, int indentTabs)
        {
            this._builder = builder;
            this._indentTabs = indentTabs;
        }

        public void AppendLine(string line)
        {
            this._builder.AppendLine(line);
        }

        public void AppendLineStart(int tabs, string line)
        {
            this._builder.Append(' ', (this._indentTabs + tabs) * 4);
            this._builder.AppendLine(line);
        }

        public void AppendStart(int tabs, string line)
        {
            this._builder.Append(' ', (this._indentTabs + tabs) * 4);
            this._builder.Append(line);
        }

        public void Append(string line)
        {
            this._builder.Append(line);
        }
    }
}