using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Linq; using static System.Console; using System.Xml.Linq; using SoftEther.JsonRpc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Markdig; namespace VPNServer_JSONRPC_CodeGen { public enum TargetLang { CSharp, TypeScript, } static class CodeGenUtil { public static string AppExeDir; public static string ProjectDir; public static string VpnSrcDir; public static string OutputDir_Clients; public static string OutputDir_HamCore; static CodeGenUtil() { AppExeDir = System.AppContext.BaseDirectory; ProjectDir = AppExeDir; string tmp = AppExeDir; while (true) { try { tmp = Path.GetDirectoryName(tmp); if (Directory.GetFiles(tmp, "*.csproj").Length >= 1) { ProjectDir = tmp; break; } } catch { break; } } OutputDir_Clients = Path.Combine(ProjectDir, @"..\vpnserver-jsonrpc-clients"); string root_dir = Path.Combine(ProjectDir, @"..\.."); string dirname = null; if (Directory.Exists(Path.Combine(root_dir, "Main"))) dirname = "Main"; if (Directory.Exists(Path.Combine(root_dir, "src"))) dirname = "src"; if (string.IsNullOrEmpty(dirname)) throw new ApplicationException($"Directory '{root_dir}' is not a root dir."); VpnSrcDir = dirname; OutputDir_HamCore = Path.Combine(root_dir, dirname, @"bin\hamcore"); if (Directory.Exists(OutputDir_HamCore) == false) throw new ApplicationException($"Direction '{OutputDir_HamCore}' not found."); } public static void MakeDir(string path) { try { Directory.CreateDirectory(path); } catch { } } } class CSharpSourceCode { public SyntaxTree Tree { get; } public CompilationUnitSyntax Root { get; } public SemanticModel Model { get; set; } public CSharpSourceCode(string filename) : this(File.ReadAllText(filename), filename) { } public CSharpSourceCode(string body, string filename) { this.Tree = CSharpSyntaxTree.ParseText(body, path: filename); this.Root = this.Tree.GetCompilationUnitRoot(); } } class CSharpCompiler { public string AssemblyName { get; } public List ReferencesList { get; } = new List(); public List SourceCodeList { get; } = new List(); CSharpCompilation _compilation = null; public CSharpCompilation Compilation { get { if (_compilation == null) { _compilation = CSharpCompilation.Create(this.AssemblyName, this.SourceCodeList.Select(s => s.Tree), this.ReferencesList, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Debug, assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); } return _compilation; } } public CSharpCompiler(string assembly_name) { this.AssemblyName = assembly_name; } public void AddReference(MetadataReference r) { this.ReferencesList.Add(r); } public void AddReferenceByPath(string path) { AddReference(MetadataReference.CreateFromFile(path)); } public void AddReferenceByType(Type type) { AddReferenceByPath(type.Assembly.Location); } public void AddReferenceByAssemblyName(string name) { var a = System.Reflection.Assembly.Load(new System.Reflection.AssemblyName(name)); AddReferenceByPath(a.Location); } public void AddReferenceDotNetStandard() { var a = System.Reflection.Assembly.Load(new System.Reflection.AssemblyName("netstandard")); AddReferenceByPath(a.Location); string dir = Path.GetDirectoryName(a.Location); AddReferenceByPath(Path.Combine(dir, "System.Private.CoreLib.dll")); foreach (var refa in a.GetReferencedAssemblies()) { string dll_name = Path.Combine(dir, refa.Name) + ".dll"; if (File.Exists(dll_name)) { AddReferenceByPath(dll_name); } } } public void AddSourceCode(CSharpSourceCode cs) { this.SourceCodeList.Add(cs); } public bool OkOrPrintErrors() { MemoryStream ms = new MemoryStream(); Microsoft.CodeAnalysis.Emit.EmitResult ret = Compilation.Emit(ms); if (ret.Success) { return true; } IEnumerable failures = ret.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); foreach (Diagnostic diagnostic in failures) { WriteLine(diagnostic.ToString()); } return false; } public void Compile(bool test_full_compile = false) { if (test_full_compile) { if (OkOrPrintErrors() == false) { throw new ApplicationException("Compile Error."); } } foreach (CSharpSourceCode cs in this.SourceCodeList) { cs.Model = this.Compilation.GetSemanticModel(cs.Tree); } } } class GeneratedCodePart { public int Seq = 0; public string Text = ""; } class GeneratedCodeSection { public List PartList = new List(); public override string ToString() { StringWriter w = new StringWriter(); var a = this.PartList.OrderBy(x => x.Seq); foreach (var b in a) { w.Write(b.Text.ToString()); } return w.ToString(); } public void AddPart(int seq, string text) { this.PartList.Add(new GeneratedCodePart() { Seq = seq, Text = text }); } } class GeneratedCode { public GeneratedCodeSection Types = new GeneratedCodeSection(); public GeneratedCodeSection Stubs = new GeneratedCodeSection(); public GeneratedCodeSection Tests = new GeneratedCodeSection(); public override string ToString() { StringWriter w = new StringWriter(); w.WriteLine("// --- Types ---"); w.Write(this.Types.ToString()); w.WriteLine(); w.WriteLine("// --- Stubs ---"); w.Write(this.Stubs.ToString()); w.WriteLine(); w.WriteLine("// --- Tests ---"); w.Write(this.Tests.ToString()); w.WriteLine(); return w.ToString(); } } class GeneratedCodeForLang { public GeneratedCode TypeScript = new GeneratedCode(); public string DocsRpc = ""; } static class CodeGenExtensions { public static string GetDocumentStr(this ISymbol sym) { if (sym == null) return ""; string xml = sym.GetDocumentationCommentXml(); if (string.IsNullOrEmpty(xml)) return ""; XDocument doc = XDocument.Parse(xml); var summary = doc.Descendants("summary").FirstOrDefault(); string str = summary.Value; if (string.IsNullOrEmpty(str)) return ""; str = str.Replace(" (Async mode)", "", StringComparison.InvariantCultureIgnoreCase); str = str.Trim(); return str; } } class RpcInfo { public string Name; public string TypeName; public IMethodSymbol Symbol; public HashSet InputParamMembers = new HashSet(); } class RpcTypeParameterInfo { public string Name; public string Type; public string Description; } class RpcTypeInfo { public string Name; public string Description; public List Params = new List(); public List SubTypes = new List(); } class CodeGen { CSharpSourceCode cs_types, cs_stubs, cs_tests; public Dictionary rpc_list = new Dictionary(); public Dictionary rpc_type_list = new Dictionary(); CSharpCompiler csc; public CodeGen() { csc = new CSharpCompiler("Test"); csc.AddReferenceDotNetStandard(); csc.AddReferenceByType(typeof(Newtonsoft.Json.JsonPropertyAttribute)); cs_types = new CSharpSourceCode(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpc\VPNServerRpcTypes.cs")); csc.AddSourceCode(cs_types); cs_stubs = new CSharpSourceCode(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpc\VPNServerRpc.cs")); csc.AddSourceCode(cs_stubs); cs_tests = new CSharpSourceCode(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpcTest\VpnServerRpcTest.cs")); csc.AddSourceCode(cs_tests); csc.Compile(); } void generate_types(GeneratedCodeForLang ret) { var model = cs_types.Model; var class_list = cs_types.Root.DescendantNodes().OfType(); foreach (ClassDeclarationSyntax c in class_list) { StringWriter ts = new StringWriter(); string doc = model.GetDeclaredSymbol(c).GetDocumentStr(); if (string.IsNullOrEmpty(doc) == false) { ts.WriteLine($"/** {doc} */"); } RpcTypeInfo info = new RpcTypeInfo() { Name = c.Identifier.Text, Description = doc, }; rpc_type_list[c.Identifier.Text] = info; ts.WriteLine($"export class {c.Identifier.Text}"); ts.WriteLine("{"); foreach (var member in model.GetDeclaredSymbol(c).GetMembers()) { string json_name = ""; bool json_name_has_special_char = false; var atts = member.GetAttributes(); var y = atts.Where(x => x.AttributeClass.Name == "JsonPropertyAttribute").FirstOrDefault(); if (y != null) { json_name = y.ConstructorArguments.FirstOrDefault().Value.ToString(); if (json_name.IndexOf(':') != -1 || json_name.IndexOf('.') != -1) json_name_has_special_char = true; } string default_value = "\"\""; string enum_type = ""; switch (member) { case IFieldSymbol field: string ts_type = ""; ITypeSymbol type = field.Type; switch (type.Kind) { case SymbolKind.NamedType: switch (type.Name) { case "UInt32": case "UInt64": ts_type = "number"; default_value = "0"; break; case "String": ts_type = "string"; break; case "Boolean": ts_type = "boolean"; default_value = "false"; break; case "DateTime": ts_type = "Date"; default_value = "new Date()"; break; default: if (type.TypeKind == TypeKind.Enum) { ts_type = type.Name; enum_type = type.Name; default_value = "0"; break; } throw new ApplicationException($"{c.Identifier}.{member.Name}: type.Name = {type.Name}"); } break; case SymbolKind.ArrayType: ITypeSymbol type2 = ((IArrayTypeSymbol)type).ElementType; default_value = "[]"; switch (type2.Kind) { case SymbolKind.NamedType: switch (type2.Name) { case "UInt32": case "UInt64": ts_type = "number[]"; break; case "String": ts_type = "string[]"; break; case "Boolean": ts_type = "boolean[]"; break; case "Byte": ts_type = "Uint8Array"; default_value = "new Uint8Array([])"; break; default: if (type2.ContainingAssembly.Name == csc.AssemblyName) { ts_type = type2.Name + "[]"; enum_type = type2.Name; break; } throw new ApplicationException($"{c.Identifier}.{member.Name}: type2.Name = {type2.Name}"); } break; default: throw new ApplicationException($"{c.Identifier}.{member.Name}: type2.Kind = {type2.Kind}"); } break; default: throw new ApplicationException($"{c.Identifier}.{member.Name}: type.Kind = {type.Kind}"); } if (string.IsNullOrEmpty(ts_type) == false) { string field_name = field.Name; string doc2 = member.GetDocumentStr(); if (string.IsNullOrEmpty(json_name) == false) field_name = json_name; string info_type = ts_type; string info_type2 = ""; if (field_name.EndsWith("_str")) info_type2 = "ASCII"; if (field_name.EndsWith("_utf")) info_type2 = "UTF8"; if (field_name.EndsWith("_ip")) info_type2 = "IP address"; if (field_name.EndsWith("_u32")) info_type2 = "uint32"; if (field_name.EndsWith("_u64")) info_type2 = "uint64"; if (field_name.EndsWith("_bin")) { info_type2 = "Base64 binary"; info_type = "string"; } string docs_add = ""; if (string.IsNullOrEmpty(enum_type) == false) { Type et = Type.GetType("SoftEther.VPNServerRpc." + enum_type); if (et.IsEnum) { docs_add += "
Values:"; var ed = cs_types.Root.DescendantNodes().OfType() .Where(e => e.Identifier.Text == enum_type) .Single(); foreach (var em in model.GetDeclaredSymbol(ed).GetMembers()) { switch (em) { case IFieldSymbol ef: if (ef.IsConst && ef.IsDefinition) { string doc3 = em.GetDocumentStr(); docs_add += $"
`{ef.ConstantValue}`: {doc3}"; } break; } } info_type = "number"; info_type2 = "enum"; } else { if (info.SubTypes.Contains(enum_type) == false) { info.SubTypes.Add(enum_type); info_type = "Array object"; } } } info_type = "`" + info_type + "`"; if (string.IsNullOrEmpty(info_type2) == false) info_type += " (" + info_type2 + ")"; info.Params.Add(new RpcTypeParameterInfo() { Name = field_name, Type = info_type, Description = doc2 + docs_add, }); if (json_name_has_special_char) field_name = $"[\"{json_name}\"]"; if (string.IsNullOrEmpty(doc2) == false) { ts.WriteLine($" /** {doc2} */"); } ts.WriteLine($" public {field_name}: {ts_type} = {default_value};"); ts.WriteLine(); } break; case IMethodSymbol method when method.MethodKind == MethodKind.Constructor: break; default: throw new ApplicationException($"{c.Identifier}.{member.Name}: type = {member.GetType()}"); } } if (string.IsNullOrEmpty(doc) == false) { ts.WriteLine($" /** Constructor for the '{c.Identifier.Text}' class: {doc} */"); } ts.WriteLine($" public constructor(init?: Partial<{c.Identifier.Text}>)"); ts.WriteLine(" {"); ts.WriteLine(" Object.assign(this, init);"); ts.WriteLine(" }"); ts.WriteLine("}"); ts.WriteLine(); ret.TypeScript.Types.AddPart(c.SpanStart, ts.ToString()); } var enum_list = cs_types.Root.DescendantNodes().OfType(); foreach (EnumDeclarationSyntax e in enum_list) { StringWriter ts = new StringWriter(); string doc = model.GetDeclaredSymbol(e).GetDocumentStr(); if (string.IsNullOrEmpty(doc) == false) { ts.WriteLine($"/** {doc} */"); } ts.WriteLine($"export enum {e.Identifier.Text}"); ts.WriteLine("{"); foreach (var member in model.GetDeclaredSymbol(e).GetMembers()) { switch (member) { case IFieldSymbol field: if (field.IsConst && field.IsDefinition) { string doc2 = member.GetDocumentStr(); if (string.IsNullOrEmpty(doc2) == false) { ts.WriteLine($" /** {doc2} */"); } ts.WriteLine($" {field.Name} = {field.ConstantValue},"); ts.WriteLine(); } break; } } ts.WriteLine("}"); ts.WriteLine(); ret.TypeScript.Types.AddPart(e.SpanStart, ts.ToString()); } } void generate_stubs(GeneratedCodeForLang ret) { var model = cs_stubs.Model; var rpc_class = cs_stubs.Root.DescendantNodes().OfType().Where(c => c.Identifier.Text == "VpnServerRpc").First(); var members = model.GetDeclaredSymbol(rpc_class).GetMembers(); var methods = members.Where(m => m is IMethodSymbol).Select(m => m as IMethodSymbol).Where(m => m.IsStatic == false) .Where(m => m.IsAsync).Where(m => m.Name != "CallAsync"); foreach (var method in methods) { string method_name = method.Name; if (method_name.EndsWith("Async") == false) throw new ApplicationException($"{method.Name}: method_name = {method_name}"); method_name = method_name.Substring(0, method_name.Length - 5); INamedTypeSymbol ret_type = (INamedTypeSymbol)method.ReturnType; if (ret_type.Name != "Task") throw new ApplicationException($"{method.Name}: ret_type.Name = {ret_type.Name}"); var ret_type_args = ret_type.TypeArguments; if (ret_type_args.Length != 1) throw new ApplicationException($"{method.Name}: type_args.Length = {ret_type_args.Length}"); var ret_type_name = ret_type_args[0].Name; if (method.Parameters.Length >= 2) throw new ApplicationException($"{method.Name}: method.Parameters.Length = {method.Parameters.Length}"); if (method.DeclaringSyntaxReferences.Length != 1) throw new ApplicationException($"{method.Name}: method.DeclaringSyntaxReferences.Length = {method.DeclaringSyntaxReferences.Length}"); MethodDeclarationSyntax syntax = (MethodDeclarationSyntax)method.DeclaringSyntaxReferences[0].GetSyntax(); if (syntax.Body != null) throw new ApplicationException($"{method.Name}: syntax.Body != null"); if (syntax.ExpressionBody == null) throw new ApplicationException($"{method.Name}: syntax.ExpressionBody == null"); ArrowExpressionClauseSyntax body = syntax.ExpressionBody; InvocationExpressionSyntax invoke = body.DescendantNodes().OfType().Single(); if (model.GetSymbolInfo(invoke.Expression).Symbol.Name != "CallAsync") throw new ApplicationException($"{method.Name}: model.GetSymbolInfo(invoke.Expression).Symbol.Name = {model.GetSymbolInfo(invoke.Expression).Symbol.Name}"); if (invoke.ArgumentList.Arguments.Count != 2) throw new ApplicationException($"{method.Name}: invoke.ArgumentList.Arguments.Count = {invoke.ArgumentList.Arguments.Count}"); LiteralExpressionSyntax str_syntax = (LiteralExpressionSyntax)invoke.ArgumentList.Arguments[0].Expression; string str = str_syntax.Token.Text; StringWriter ts = new StringWriter(); string doc2 = method.GetDocumentStr(); if (string.IsNullOrEmpty(doc2) == false) { ts.WriteLine($" /** {doc2} */"); } if (method.Parameters.Length == 0) { ts.WriteLine($" public {method_name} = (): Promise<{ret_type_name}> =>"); ts.WriteLine(" {"); ts.WriteLine($" return this.CallAsync<{ret_type_name}>({str}, new {ret_type_name}());"); ts.WriteLine(" }"); ts.WriteLine(" "); } else { ts.WriteLine($" public {method_name} = (in_param: {ret_type_name}): Promise<{ret_type_name}> =>"); ts.WriteLine(" {"); ts.WriteLine($" return this.CallAsync<{ret_type_name}>({str}, in_param);"); ts.WriteLine(" }"); ts.WriteLine(" "); } rpc_list[method_name] = new RpcInfo() { Name = method_name, TypeName = ret_type_name, Symbol = method, }; ret.TypeScript.Stubs.AddPart(method.DeclaringSyntaxReferences[0].Span.Start, ts.ToString()); } } class CcWalker : CSharpSyntaxWalker { StringWriter w = new StringWriter(); List lines = new List(); string current_line = ""; int current_depth = 0; const int TabSpace = 4; CSharpSourceCode src; TargetLang lang; public CcWalker(CSharpSourceCode src, TargetLang lang) : base(SyntaxWalkerDepth.StructuredTrivia) { this.src = src; this.lang = lang; } string convert_type(string src) { if (lang == TargetLang.TypeScript) { if (src.StartsWith("Vpn")) { src = "VPN." + src; } if (src == "int" || src == "uint" || src == "long" || src == "ulong") { src = "number"; } if (src == "bool") { src = "boolean"; } if (src == "DateTime") { src = "Date"; } } return src; } string convert_function(string src) { if (lang == TargetLang.TypeScript) { if (src == "Console.WriteLine" || src == "print_object") { src = "console.log"; } if (src.StartsWith("api.") || src.StartsWith("Test_")) { src = "await " + src; } } return src; } void _emit_internal(string str, bool new_line) { if (string.IsNullOrEmpty(current_line)) { current_line += new string(' ', current_depth * TabSpace); } current_line += str; if (new_line) { lines.Add(current_line); current_line = ""; } } void emit_line(string str = "") => emit(str + "\r\n"); void emit(string str, bool new_line) { if (new_line == false) { emit(str); } else { emit_line(str); } } void emit(string str) { string tmp = ""; for (int i = 0; i < str.Length; i++) { char c = str[i]; if (c == '\r') { } else if (c == '\n') { _emit_internal(tmp, true); tmp = ""; } else { tmp += c; } } if (String.IsNullOrEmpty(tmp) == false) { _emit_internal(tmp, false); } } public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { if (node.Identifier.Text == "print_object") return; if (lang == TargetLang.TypeScript) { emit_line(); var sem = src.Model.GetDeclaredSymbol(node); string doc2 = sem.GetDocumentStr(); if (string.IsNullOrEmpty(doc2) == false) { emit_line($"/** {doc2} */"); } emit("async function "); emit(node.Identifier.Text); Visit(node.ParameterList); emit(": "); emit("Promise<"); Visit(node.ReturnType); emit(">"); emit_line(""); Visit(node.Body); } else { emit("public"); emit(" "); Visit(node.ReturnType); emit(" "); emit(node.Identifier.Text); Visit(node.ParameterList); emit_line(""); Visit(node.Body); } } public override void VisitParameter(ParameterSyntax node) { if (lang == TargetLang.TypeScript) { emit($"{node.Identifier.Text}"); emit(": "); Visit(node.Type); } else { Visit(node.Type); emit(" "); emit($"{node.Identifier.Text}"); } } public override void VisitParameterList(ParameterListSyntax node) { emit("("); int num = 0; foreach (ParameterSyntax p in node.Parameters) { if (num >= 1) { emit(", "); } Visit(p); num++; } emit(")"); } public override void VisitArgumentList(ArgumentListSyntax node) { emit("("); int num = 0; foreach (ArgumentSyntax arg in node.Arguments) { if (num >= 1) { emit(", "); } this.VisitArgument(arg); num++; } emit(")"); } public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) { if (lang == TargetLang.TypeScript) { if (node.Parent.Kind() == SyntaxKind.ObjectInitializerExpression) { Visit(node.Left); emit(": "); Visit(node.Right); } else { Visit(node.Left); emit(" = "); Visit(node.Right); } } else { Visit(node.Left); emit(" = "); Visit(node.Right); } } public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) { Visit(node.Expression); emit(node.OperatorToken.Text); Visit(node.Name); } public override void VisitCastExpression(CastExpressionSyntax node) { if (lang == TargetLang.TypeScript) { Visit(node.Expression); } else { emit("("); Visit(node.Type); emit(")"); Visit(node.Expression); } } public override void VisitBreakStatement(BreakStatementSyntax node) { emit_line("break;"); } public override void VisitReturnStatement(ReturnStatementSyntax node) { if (node.Expression == null) { emit_line("return;"); } else { emit("return"); emit(" "); Visit(node.Expression); emit_line(";"); } } public override void VisitForEachStatement(ForEachStatementSyntax node) { if (lang == TargetLang.TypeScript) { emit("for (let "); emit(node.Identifier.Text); emit(" of "); Visit(node.Expression); emit_line(")"); Visit(node.Statement); } else { emit("foreach ("); Visit(node.Type); emit(" "); emit(node.Identifier.Text); emit(" in "); Visit(node.Expression); emit_line(")"); Visit(node.Statement); } } public override void VisitExpressionStatement(ExpressionStatementSyntax node) { Visit(node.Expression); emit_line(";"); } public override void VisitConditionalExpression(ConditionalExpressionSyntax node) { Visit(node.Condition); emit(" ? "); Visit(node.WhenTrue); emit(" : "); Visit(node.WhenFalse); } public override void VisitIfStatement(IfStatementSyntax node) { emit("if ("); Visit(node.Condition); emit_line(")"); Visit(node.Statement); if (node.Else != null) { if (node.Else.Statement is IfStatementSyntax) { emit("else "); } else { emit_line("else"); } Visit(node.Else.Statement); } } public override void VisitInitializerExpression(InitializerExpressionSyntax node) { if (lang == TargetLang.TypeScript) { if (node.Kind() == SyntaxKind.ArrayInitializerExpression) { bool is_byte_array = false; if (node.Parent.Kind() == SyntaxKind.ArrayCreationExpression && ((ArrayCreationExpressionSyntax)node.Parent).Type.ElementType.ToString() == "byte") { is_byte_array = true; } if (is_byte_array) { emit("new Uint8Array("); } emit("[ "); current_depth++; foreach (var exp in node.Expressions) { this.Visit(exp); emit(", "); } current_depth--; emit(" ]"); if (is_byte_array) { emit(")"); } } else { emit_line("{"); current_depth++; foreach (var exp in node.Expressions) { this.Visit(exp); emit_line(","); } current_depth--; emit("}"); } } else { if (node.Kind() == SyntaxKind.ArrayInitializerExpression) { emit("{ "); current_depth++; foreach (var exp in node.Expressions) { this.Visit(exp); emit(", "); } current_depth--; emit(" }"); } else { emit_line("{"); current_depth++; foreach (var exp in node.Expressions) { this.Visit(exp); emit_line(","); } current_depth--; emit("}"); } } } public override void VisitArrayCreationExpression(ArrayCreationExpressionSyntax node) { if (lang == TargetLang.TypeScript) { var type = node.Type; if (node.Initializer != null) { emit(" "); Visit(node.Initializer); } else { emit("[]"); } } else { var type = node.Type; emit("new "); Visit(node.Type); if (node.Initializer != null) { emit(" "); Visit(node.Initializer); } } } public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) { if (lang == TargetLang.TypeScript) { var type = (IdentifierNameSyntax)node.Type; if (node.Initializer == null) { emit("new "); Visit(node.Type); // emit($"new {type.Identifier.Text}"); Visit(node.ArgumentList); } else { emit("new "); Visit(node.Type); emit_line("("); Visit(node.Initializer); emit(")"); } } else { var type = (IdentifierNameSyntax)node.Type; emit($"new {type.Identifier.Text}"); Visit(node.ArgumentList); if (node.Initializer != null) { emit_line(""); Visit(node.Initializer); } } } public override void VisitLiteralExpression(LiteralExpressionSyntax node) { emit(node.Token.Text); } public override void VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) { emit("("); base.Visit(node.Expression); emit(")"); } public override void VisitBinaryExpression(BinaryExpressionSyntax node) { base.Visit(node.Left); emit($" {node.OperatorToken.Text} "); base.Visit(node.Right); } public override void VisitIdentifierName(IdentifierNameSyntax node) { string name = node.Identifier.Text; if (node.Parent.Kind() == SyntaxKind.VariableDeclaration || node.Parent.Kind() == SyntaxKind.MethodDeclaration || node.Parent.Kind() == SyntaxKind.SimpleMemberAccessExpression || node.Parent.Kind() == SyntaxKind.ForEachStatement || node.Parent.Kind() == SyntaxKind.Parameter || node.Parent.Kind() == SyntaxKind.ObjectCreationExpression) { name = convert_type(name); } var sym = src.Model.GetSymbolInfo(node); string json_name = ""; bool json_name_has_special_char = false; var atts = sym.Symbol.GetAttributes(); var y = atts.Where(x => x.AttributeClass.Name == "JsonPropertyAttribute").FirstOrDefault(); if (y != null) { json_name = y.ConstructorArguments.FirstOrDefault().Value.ToString(); if (json_name.IndexOf(':') != -1 || json_name.IndexOf('.') != -1) json_name_has_special_char = true; } string field_name = name; if (lang == TargetLang.TypeScript) { if (string.IsNullOrEmpty(json_name) == false) field_name = json_name; if (json_name_has_special_char) field_name = $"[\"{json_name}\"]"; } emit(field_name); } public override void VisitInvocationExpression(InvocationExpressionSyntax node) { string func_name = node.Expression.ToString(); func_name = convert_function(func_name); if (lang == TargetLang.TypeScript) { if (func_name == "rand.Next") { string a = node.ArgumentList.Arguments[0].ToString(); string b = node.ArgumentList.Arguments[1].ToString(); emit($"Math.floor((Math.random() * ({b} - {a})) + {a})"); return; } if (func_name == "System.Threading.Thread.Sleep") { string a = node.ArgumentList.Arguments[0].ToString(); emit($"await new Promise((r) => setTimeout(r, {a}))"); return; } } emit(func_name); Visit(node.ArgumentList); } public override void VisitPredefinedType(PredefinedTypeSyntax node) { string name = node.Keyword.Text; name = convert_type(name); emit(name); } public override void VisitArrayRankSpecifier(ArrayRankSpecifierSyntax node) { emit("["); int num = 0; foreach (ExpressionSyntax exp in node.Sizes) { if (num >= 1) { emit(","); } Visit(exp); num++; } emit("]"); } public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) { /*foreach (var statement in node.Body.Statements) { Visit(statement); }*/ } public override void VisitArrayType(ArrayTypeSyntax node) { Visit(node.ElementType); foreach (var rank in node.RankSpecifiers) { Visit(rank); } } public void VisitVariableDeclarator(VariableDeclaratorSyntax node, TypeSyntax type) { if (lang == TargetLang.TypeScript) { // if (node.Parent.Parent.Kind() == SyntaxKind.LocalDeclarationStatement) { emit("let "); } emit($"{node.Identifier.Text}"); emit(": "); var type_dec = src.Model.GetTypeInfo(type); if (type is PredefinedTypeSyntax) { Visit(type); } else if (type is ArrayTypeSyntax) { Visit(type); } else if (type is IdentifierNameSyntax) { Visit(type); } else { throw new ApplicationException($"VisitVariableDeclarator: {type.GetType().ToString()}"); } if (node.Initializer != null) { emit(" = "); var value = node.Initializer.Value; base.Visit(value); } emit_line(";"); } else { var type_dec = src.Model.GetTypeInfo(type); if (type is PredefinedTypeSyntax) { Visit(type); } else if (type is ArrayTypeSyntax) { Visit(type); } else if (type is IdentifierNameSyntax) { Visit(type); } else { throw new ApplicationException($"VisitVariableDeclarator: {type.GetType().ToString()}"); } emit($" {node.Identifier.Text}"); if (node.Initializer != null) { emit(" = "); var value = node.Initializer.Value; base.Visit(value); } emit_line(";"); } } public override void VisitVariableDeclaration(VariableDeclarationSyntax node) { foreach (var v in node.Variables) { VisitVariableDeclarator(v, node.Type); } } public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) { Visit(node.Declaration); } public override void VisitFieldDeclaration(FieldDeclarationSyntax node) { //Visit(node.Declaration); } public override void VisitBlock(BlockSyntax node) { emit_line("{"); current_depth++; foreach (var statement in node.Statements) { Visit(statement); } current_depth--; emit_line("}"); } public override void VisitClassDeclaration(ClassDeclarationSyntax node) { if (lang == TargetLang.TypeScript) { base.VisitClassDeclaration(node); } else { emit_line($"class {node.Identifier.Text}"); emit_line("{"); current_depth++; base.VisitClassDeclaration(node); current_depth--; emit_line("}"); } } public override string ToString() { StringWriter w = new StringWriter(); this.lines.ForEach(x => w.WriteLine(x)); if (String.IsNullOrEmpty(this.current_line) == false) w.WriteLine(this.current_line); return w.ToString(); } } void generate_tests(GeneratedCodeForLang ret) { var test_class = cs_tests.Root.DescendantNodes().OfType().Where(c => c.Identifier.Text == "VPNRPCTest").First(); CcWalker ts_walker = new CcWalker(cs_tests, TargetLang.TypeScript); ts_walker.Visit(test_class); ret.TypeScript.Tests.PartList.Add(new GeneratedCodePart() { Seq = 0, Text = ts_walker.ToString() }); } void doc_write_parameters(StringWriter w, RpcTypeInfo type_info) { List plist = new List(); foreach (RpcTypeParameterInfo p in type_info.Params) { plist.Add(p); } foreach (string subtype in type_info.SubTypes) { foreach (RpcTypeParameterInfo p in rpc_type_list[subtype].Params) { plist.Add(p); } } w.WriteLine("Name | Type | Description"); w.WriteLine("--- | --- | ---"); foreach (RpcTypeParameterInfo p in plist) { w.WriteLine($"`{p.Name}` | {p.Type} | {p.Description}"); } } void doc_write_function(StringWriter w, RpcInfo rpc) { string func_summary = rpc.Symbol.GetDocumentStr(); int index = func_summary.IndexOf("."); if (index != -1) func_summary = func_summary.Substring(0, index + 1); func_summary = func_summary.TrimEnd('.'); w.WriteLine($""); w.WriteLine($"## \"{rpc.Name}\" RPC API - {func_summary}"); w.WriteLine("### Description"); w.WriteLine(rpc.Symbol.GetDocumentStr()); var model = cs_tests.Model; var func = cs_tests.Root.DescendantNodes().OfType() .Where(f => f.Identifier.Text == "Test_" + rpc.Name) .Single(); var fields = func.DescendantNodes().OfType() .Where(i => i.Kind() == SyntaxKind.ObjectInitializerExpression) .SelectMany(o => o.DescendantNodes().OfType()) .Where(a => a.Kind() == SyntaxKind.SimpleAssignmentExpression) .Select(a => (a.Left as IdentifierNameSyntax)); foreach (var field in fields) { string json_name = field.Identifier.Text; var sym = model.GetSymbolInfo(field); var atts = sym.Symbol.GetAttributes(); var y = atts.Where(x => x.AttributeClass.Name == "JsonPropertyAttribute").FirstOrDefault(); if (y != null) { json_name = y.ConstructorArguments.FirstOrDefault().Value.ToString(); } rpc.InputParamMembers.Add(json_name); } Type obj_type = Type.GetType("SoftEther.VPNServerRpc." + rpc.TypeName); object in_object = Activator.CreateInstance(obj_type); object out_object = Activator.CreateInstance(obj_type); JsonRpcRequest rpc_in = new JsonRpcRequest() { Method = rpc.Name, Params = in_object, Id = "rpc_call_id", }; Type rpc_out_type = typeof(JsonRpcResponse<>).MakeGenericType(obj_type); var rpc_out = Activator.CreateInstance(rpc_out_type); rpc_out_type.GetProperty("Id").SetValue(rpc_out, "rpc_call_id"); rpc_out_type.GetProperty("Result").SetValue(rpc_out, out_object); sample_fill_object(in_object); sample_fill_object(out_object); JsonSerializerSettings rpc_in_settings = new JsonSerializerSettings() { MaxDepth = 8, NullValueHandling = NullValueHandling.Include, ReferenceLoopHandling = ReferenceLoopHandling.Error, PreserveReferencesHandling = PreserveReferencesHandling.None, ContractResolver = new JSonInputContractResolver(rpc), }; JsonSerializerSettings rpc_out_settings = new JsonSerializerSettings() { MaxDepth = 8, NullValueHandling = NullValueHandling.Include, ReferenceLoopHandling = ReferenceLoopHandling.Error, PreserveReferencesHandling = PreserveReferencesHandling.None, ContractResolver = new JSonOutputContractResolver(rpc), }; string in_str = JsonConvert.SerializeObject(rpc_in, Formatting.Indented, rpc_in_settings); string out_str = JsonConvert.SerializeObject(rpc_out, Formatting.Indented, rpc_out_settings); w.WriteLine(); w.WriteLine("### Input JSON-RPC Format"); w.WriteLine("```json"); w.WriteLine(in_str); w.WriteLine("```"); w.WriteLine(); w.WriteLine("### Output JSON-RPC Format"); w.WriteLine("```json"); w.WriteLine(out_str); w.WriteLine("```"); w.WriteLine(); w.WriteLine("### Parameters"); w.WriteLine(); doc_write_parameters(w, rpc_type_list[rpc.TypeName]); //w.WriteLine("
"); w.WriteLine(); } class JSonOutputContractResolver : DefaultContractResolver { RpcInfo rpc_info; public JSonOutputContractResolver(RpcInfo info) : base() { this.rpc_info = info; } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { if (member.Name == "Error") return null; JsonProperty ret = base.CreateProperty(member, memberSerialization); return ret; } } class JSonInputConverter : JsonConverter { RpcInfo rpc_info; public JSonInputConverter(RpcInfo info) { this.rpc_info = info; } public override bool CanRead => false; public override bool CanConvert(Type objectType) { return true; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override void WriteJson(JsonWriter w, object value, JsonSerializer serializer) { JToken t = JToken.FromObject(value); List a = new List(); bool all = false; if (rpc_info.Name == "SetHubLog") all = true; foreach (var p1 in t.Children()) { foreach (var p2 in p1.Children()) { if (rpc_info.InputParamMembers.Contains(p2.Name) == false) a.Add(p2); } if (rpc_info.InputParamMembers.Contains(p1.Name) == false) a.Add(p1); } if (all == false) { foreach (var p in a) { try { p.Remove(); } catch { } } } t.WriteTo(w); } } class JSonInputContractResolver : DefaultContractResolver { RpcInfo rpc_info; public JSonInputContractResolver(RpcInfo info) : base() { this.rpc_info = info; } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty ret = base.CreateProperty(member, memberSerialization); ret.Converter = new JSonInputConverter(this.rpc_info); return ret; } } void sample_fill_object(object o) { Type t = o.GetType(); var fields = t.GetFields(); foreach (var field in fields) { Type t2 = field.FieldType; object v = null; if (t2 == typeof(string)) { string tmp = field.Name.ToLowerInvariant(); if (tmp.EndsWith("_str") || tmp.EndsWith("_utf")) tmp = tmp.Substring(0, tmp.Length - 4); if (tmp.EndsWith("_ip")) { if (tmp.IndexOf("mask", StringComparison.InvariantCultureIgnoreCase) == -1) tmp = "192.168.0.1"; else tmp = "255.255.255.255"; } v = tmp; } else if (t2 == typeof(uint)) v = (uint)0; else if (t2 == typeof(ulong)) v = (ulong)0; else if (t2 == typeof(bool)) v = (bool)false; else if (t2 == typeof(byte[])) v = Encoding.UTF8.GetBytes("Hello World"); else if (t2 == typeof(DateTime)) v = new DateTime(DateTime.Now.Year + 1, 8, 1, 12, 24, 36, 123); else if (t2.IsEnum) { v = (int)0; } else if (t2.IsArray) { if (t2 == typeof(uint[])) { v = new uint[] { 1, 2, 3 }; } else { if (t2.GetArrayRank() != 1) throw new ApplicationException("Array rank != 1"); Type obj_type = t2.GetElementType(); if (obj_type.IsEnum) { v = new int[] { 1, 2, 3 }; } else { int num = 3; if (field.Name.IndexOf("single", StringComparison.CurrentCultureIgnoreCase) != -1) { num = 1; } object list = Activator.CreateInstance(typeof(List<>).MakeGenericType(obj_type)); for (int i = 0; i < num; i++) { object a = Activator.CreateInstance(obj_type); sample_fill_object(a); list.GetType().GetMethod("Add").Invoke(list, new object[] { a }); } v = list.GetType().GetMethod("ToArray").Invoke(list, new object[] { } ); } } } else if (t2.Name.StartsWith("Vpn")) { Type obj_type = Type.GetType("SoftEther.VPNServerRpc." + t2.Name); v = Activator.CreateInstance(obj_type); sample_fill_object(v); } else { throw new ApplicationException($"sample_fill_object: type: {t2.ToString()}"); } field.SetValue(o, v); } } void generate_documents(GeneratedCodeForLang ret) { StringWriter w = new StringWriter(); string doc_txt = read_text_resource("doc.txt"); w.WriteLine(doc_txt); w.WriteLine("## Table of contents"); foreach (RpcInfo rpc in rpc_list.Values) { string func_summary = rpc.Symbol.GetDocumentStr(); int index = func_summary.IndexOf("."); if (index != -1) func_summary = func_summary.Substring(0, index + 1); func_summary = func_summary.TrimEnd('.'); w.WriteLine($"- [{rpc.Name} - {func_summary}](#{rpc.Name.ToLowerInvariant()})"); } w.WriteLine(); w.WriteLine("***"); foreach (RpcInfo rpc in rpc_list.Values) { if (rpc.Name.IndexOf("Vgs", StringComparison.Ordinal) == -1) { doc_write_function(w, rpc); w.WriteLine("***"); } } w.WriteLine($"Automatically generated at {timestamp.ToString("yyyy-MM-dd HH:mm:ss")} by vpnserver-jsonrpc-codegen. "); w.WriteLine("Copyright (c) 2014-" + DateTime.Now.Year + " [SoftEther VPN Project](https://www.softether.org/) under the Apache License 2.0. "); w.WriteLine(); ret.DocsRpc = w.ToString(); } public GeneratedCodeForLang GenerateCodes() { GeneratedCodeForLang ret = new GeneratedCodeForLang(); generate_stubs(ret); generate_tests(ret); generate_types(ret); generate_documents(ret); return ret; } public void GenerateAndSaveCodes(string output_dir) { CodeGenUtil.MakeDir(output_dir); WriteLine($"GenerateAndSaveCodes(): output_dir = '{output_dir}'"); WriteLine(); WriteLine("Generating codes ..."); GeneratedCodeForLang codes = GenerateCodes(); WriteLine("Generating codes: done."); WriteLine(); output_docs(codes, output_dir); output_csharp(Path.Combine(output_dir, "vpnserver-jsonrpc-client-csharp")); output_typescript(codes.TypeScript, Path.Combine(output_dir, "vpnserver-jsonrpc-client-typescript")); } static Assembly this_assembly = Assembly.GetExecutingAssembly(); static string read_text_resource(string name) { var x = this_assembly.GetManifestResourceNames(); string resourceName = this_assembly.GetManifestResourceNames().Single(str => str.EndsWith(name)); using (Stream stream = this_assembly.GetManifestResourceStream(resourceName)) { using (StreamReader reader = new StreamReader(stream)) { return reader.ReadToEnd(); } } } static string read_text_file(string name) { using (Stream stream = File.OpenRead(name)) { using (StreamReader reader = new StreamReader(stream)) { return reader.ReadToEnd(); } } } static string replace_strings(string src, params string[] replace_list) { int i; for (i = 0; i < replace_list.Length / 2; i++) { string s1 = replace_list[i * 2]; string s2 = replace_list[i * 2 + 1]; src = src.Replace(s1, s2, StringComparison.InvariantCultureIgnoreCase); } return src; } static string normalize_crlf(string src, string crlf) { StringReader r = new StringReader(src); StringWriter w = new StringWriter(); w.NewLine = crlf; while (true) { string line = r.ReadLine(); if (line == null) break; w.WriteLine(line); } return w.ToString(); } static void normalize(ref string str, string crlf, params string[] replace_list) { str = normalize_crlf(replace_strings(str, replace_list), crlf); } static void save(string path, string body, bool bom) { string dir_name = Path.GetDirectoryName(path); CodeGenUtil.MakeDir(dir_name); if (bom) File.WriteAllText(path, body, Encoding.UTF8); else File.WriteAllText(path, body); } DateTime timestamp = DateTime.Now; void output_docs(GeneratedCodeForLang c, string output_dir) { CodeGenUtil.MakeDir(output_dir); save(Path.Combine(output_dir, "README.md"), c.DocsRpc, true); var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); string md_html_body = Markdown.ToHtml(c.DocsRpc, pipeline); string html = read_text_resource("md_html.html"); string[] replace_list = { "__BODY__", md_html_body, }; normalize(ref html, "\r\n", replace_list); save(Path.Combine(output_dir, "README.html"), html, true); save(Path.Combine(CodeGenUtil.OutputDir_HamCore, "vpnserver_api_doc.html"), html, true); } void output_typescript(GeneratedCode c, string output_dir) { CodeGenUtil.MakeDir(output_dir); string ts_rpc = read_text_resource("ts_rpc.txt"); string ts_test = read_text_resource("ts_test.txt"); string[] replace_list = { "__YEAR__", timestamp.Year.ToString(), "__TESTS__", c.Tests.ToString(), "__STUBS__", c.Stubs.ToString(), "__TYPES__", c.Types.ToString(), "__TIMESTAMP__", timestamp.ToString("yyyy-MM-dd HH:mm:ss"), }; normalize(ref ts_rpc, "\n", replace_list); normalize(ref ts_test, "\n", replace_list); save(Path.Combine(output_dir, "vpnrpc.ts"), ts_rpc, true); save(Path.Combine(output_dir, "sample.ts"), ts_test, true); save(Path.Combine(output_dir + "/../vpnserver-jsonrpc-client-nodejs-package/src/", "vpnrpc.ts"), ts_rpc, true); save(Path.Combine(output_dir + "/../vpnserver-jsonrpc-client-nodejs-package/src/", "sample.ts"), ts_test, true); } void output_csharp(string output_dir) { CodeGenUtil.MakeDir(output_dir); string cs_proj = read_text_resource("cs_proj.txt"); string cs_sln = read_text_resource("cs_sln.txt"); string cs_main = read_text_resource("cs_main.txt"); string cs_code_jsonrpc = read_text_file(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpc/JsonRpc.cs")); string cs_code_vpnserver_rpc = read_text_file(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpc/VPNServerRpc.cs")); string cs_code_vpnserver_rpc_types = read_text_file(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpc/VPNServerRpcTypes.cs")); string cs_code_vpnserver_rpc_test = read_text_file(Path.Combine(CodeGenUtil.ProjectDir, @"VpnServerRpcTest/VpnServerRpcTest.cs")); string[] replace_list = { "__YEAR__", timestamp.Year.ToString(), "__TIMESTAMP__", timestamp.ToString("yyyy-MM-dd HH:mm:ss"), }; normalize(ref cs_main, "\r\n", replace_list); normalize(ref cs_proj, "\r\n", replace_list); normalize(ref cs_sln, "\r\n", replace_list); normalize(ref cs_code_jsonrpc, "\r\n", replace_list); normalize(ref cs_code_vpnserver_rpc, "\r\n", replace_list); normalize(ref cs_code_vpnserver_rpc_types, "\r\n", replace_list); normalize(ref cs_code_vpnserver_rpc_test, "\r\n", replace_list); save(Path.Combine(output_dir, "vpnserver-jsonrpc-client-csharp.csproj"), cs_proj, true); save(Path.Combine(output_dir, "vpnserver-jsonrpc-client-csharp.sln"), cs_sln, true); save(Path.Combine(output_dir, @"rpc-stubs\JsonRpc.cs"), cs_code_jsonrpc, true); save(Path.Combine(output_dir, @"rpc-stubs\VPNServerRpc.cs"), cs_code_vpnserver_rpc, true); save(Path.Combine(output_dir, @"rpc-stubs\VPNServerRpcTypes.cs"), cs_code_vpnserver_rpc_types, true); save(Path.Combine(output_dir, @"sample\VpnServerRpcTest.cs"), cs_code_vpnserver_rpc_test, true); save(Path.Combine(output_dir, @"sample\Main.cs"), cs_main, true); } public void Test() { GeneratedCodeForLang ret = GenerateCodes(); Console.WriteLine(ret.TypeScript.ToString()); return; var model = cs_types.Model; var type_classes = cs_types.Root.DescendantNodes() .OfType(); foreach (ClassDeclarationSyntax v in type_classes) { WriteLine(v.Identifier.Text); var info = model.GetDeclaredSymbol(v); var x = info.GetMembers(); foreach (var y in x) { WriteLine(y.Name); } break; } Console.WriteLine(); } } }