private static void GenerateModuleSiteDocumentation(KalkModuleToGenerate module, string siteFolder) { if (module.Name == "KalkEngine") { module.Name = "General"; } module.Members.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.Ordinal)); module.Title = $"{module.Name} {(module.IsBuiltin ? "Functions":"Module")}"; if (module.Name.EndsWith("Intrinsics")) { module.Title = $"Intel {module.Name.Replace("Intrinsics", "")} Intrinsics"; module.Name = $"{module.Name.Replace("Intrinsics", "")}"; } var name = module.Name.ToLowerInvariant(); module.Url = $"/doc/api/{name}/"; const string templateText = @"--- title: {{module.Title}} url: {{module.Url}} --- {{~ if !module.IsBuiltin ~}} {{ module.Description }} In order to use the functions provided by this module, you need to import this module: ```kalk >>> import {{module.Name}} ``` {{~ end ~}} {{~ if (module.Title | string.contains 'Intrinsics') ~}} In order to use the functions provided by this module, you need to import this module: ```kalk >>> import HardwareIntrinsics ``` {%{~ {{NOTE do}} ~}%} These intrinsic functions are only available if your CPU supports `{{module.Name}}` features. {%{~ {{end}} ~}%} {{~ end ~}} {{~ for member in module.Members ~}} ## {{member.Name}} `{{member.Name}}{{~ if member.Params.size > 0 ~}}({{~ for param in member.Params ~}}{{ param.Name }}{{ param.IsOptional?'?':''}}{{ for.last?'':',' }}{{~ end ~}}){{~ end ~}}` {{~ if member.Description ~}} {{ member.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }} {{~ end ~}} {{~ if member.Params.size > 0 ~}} {{~ for param in member.Params ~}} - `{{ param.Name }}`: {{ param.Description}} {{~end ~}} {{~ end ~}} {{~ if member.Returns ~}} ### Returns {{ member.Returns | regex.replace `^\s{4}` '' 'm' | string.rstrip }} {{~ end ~}} {{~ if member.Remarks ~}} ### Remarks {{ member.Remarks | regex.replace `^\s{4}` '' 'm' | string.rstrip }} {{~ end ~}} {{~ if member.Example ~}} ### Example ```kalk {{ member.Example | regex.replace `^\s{4}` '' 'm' | string.rstrip }} ``` {{~ end ~}} {{~ end ~}} "; var template = Template.Parse(templateText); var apiFolder = Path.Combine(siteFolder, "doc", "api"); if (module.Title.Contains("Intel")) { apiFolder = Path.Combine(apiFolder, "intel"); module.Url = $"/doc/api/intel/{name}/"; } // Don't generate hardware.generated.md if (name == "hardware") { return; } var result = template.Render(new { module = module }, x => x.Name); File.WriteAllText(Path.Combine(apiFolder, $"{name}.generated.md"), result); }
private static List <KalkModuleToGenerate> FindIntrinsics(Compilation compilation) { var regexName = new Regex(@"^\s*\w+\s+(_\w+)"); int count = 0; var intelDoc = XDocument.Load("intel-intrinsics-data-latest.xml"); var nameToDoc = intelDoc.Descendants("intrinsic").Where(x => IntrinsicsSupports.Contains(x.Attribute("tech").Value ?? string.Empty)).ToDictionary(x => x.Attribute("name").Value, x => x); var generatedIntrinsics = new Dictionary <string, KalkIntrinsicToGenerate>(); foreach (var type in new Type[] { typeof(System.Runtime.Intrinsics.X86.Sse.X64), typeof(System.Runtime.Intrinsics.X86.Sse), typeof(System.Runtime.Intrinsics.X86.Sse2), typeof(System.Runtime.Intrinsics.X86.Sse2.X64), typeof(System.Runtime.Intrinsics.X86.Sse3), typeof(System.Runtime.Intrinsics.X86.Ssse3), typeof(System.Runtime.Intrinsics.X86.Sse41), typeof(System.Runtime.Intrinsics.X86.Sse41.X64), typeof(System.Runtime.Intrinsics.X86.Sse42), typeof(System.Runtime.Intrinsics.X86.Sse42.X64), typeof(System.Runtime.Intrinsics.X86.Aes), typeof(System.Runtime.Intrinsics.X86.Avx), typeof(System.Runtime.Intrinsics.X86.Avx2), typeof(System.Runtime.Intrinsics.X86.Bmi1), typeof(System.Runtime.Intrinsics.X86.Bmi1.X64), typeof(System.Runtime.Intrinsics.X86.Bmi2), typeof(System.Runtime.Intrinsics.X86.Bmi2.X64), }) { var x86Sse = compilation.GetTypeByMetadataName(type.FullName); foreach (var method in x86Sse.GetMembers().OfType <IMethodSymbol>().Where(x => x.IsStatic)) { if (method.Parameters.Length == 0) { continue; } var groupName = type.FullName.Substring(type.FullName.LastIndexOf('.') + 1).Replace("+", string.Empty); var docGroupName = type.Name == "X64" ? type.DeclaringType.Name : type.Name; var xmlDocStr = method.GetDocumentationCommentXml(); if (string.IsNullOrEmpty(xmlDocStr)) { continue; } var xmlDoc = XElement.Parse($"<root>{xmlDocStr.Trim()}</root>"); var elements = xmlDoc.Elements().First(); var csharpSummary = GetCleanedString(elements); var summaryTrimmed = csharpSummary.Replace("unsigned", string.Empty); var match = regexName.Match(summaryTrimmed); if (!match.Success) { continue; } var rawIntrinsicName = match.Groups[1].Value; var intrinsicName = rawIntrinsicName.TrimStart('_'); var desc = new KalkIntrinsicToGenerate { Name = intrinsicName, Class = groupName, MethodSymbol = method, IsFunc = true, }; bool hasInteldoc = false; if (nameToDoc.TryGetValue(rawIntrinsicName, out var elementIntelDoc)) { hasInteldoc = true; desc.Description = GetCleanedString(elementIntelDoc.Descendants("description").FirstOrDefault()); foreach (var parameter in elementIntelDoc.Descendants("parameter")) { desc.Params.Add(new KalkParamDescriptor(parameter.Attribute("varname").Value, parameter.Attribute("type").Value)); } desc.Description = desc.Description.Replace("[round_note]", string.Empty).Trim(); desc.Description = desc.Description + "\n\n" + csharpSummary; } else { desc.Description = csharpSummary; } // Patching special methods switch (desc.Name) { case "mm_prefetch": if (method.Name.EndsWith("0")) { desc.Name += "0"; } else if (method.Name.EndsWith("1")) { desc.Name += "1"; } else if (method.Name.EndsWith("2")) { desc.Name += "2"; } else if (method.Name.EndsWith("Temporal")) { desc.Name += "nta"; } else { goto default; } break; case "mm_round_sd": case "mm_round_ss": case "mm_round_pd": case "mm_round_ps": case "mm256_round_pd": case "mm256_round_ps": Debug.Assert(method.Name.StartsWith("Round")); var postfix = method.Name.Substring("Round".Length); Debug.Assert(hasInteldoc); if (desc.Name != "mm_round_ps" && desc.Name != "mm256_round_ps" && (desc.Params.Count >= 2 && method.Parameters.Length == 1)) { desc.Name += "1"; } if (!postfix.StartsWith("CurrentDirection")) { postfix = StandardMemberRenamer.Rename(postfix); desc.Name = $"{desc.Name}_{postfix}"; } // Remove rounding from doc var index = desc.Params.FindIndex(x => x.Name == "rounding"); if (index >= 0) { desc.Params.RemoveAt(index); } break; case "mm_rcp_ss": case "mm_rsqrt_ss": case "mm_sqrt_ss": if (method.Parameters.Length == 1) { desc.Name += "1"; } break; case "mm_sqrt_sd": case "mm_ceil_sd": case "mm_floor_sd": if (method.Parameters.Length == 1) { desc.Name += "1"; } break; default: if (hasInteldoc) { if (method.Parameters.Length != desc.Params.Count) { Console.WriteLine($"Parameters not matching for {method.ToDisplayString()}. Expecting: {desc.Params.Count} but got {method.Parameters.Length} "); } } break; } desc.CSharpName = desc.Name; desc.Names.Add(desc.Name); desc.RealReturnType = method.ReturnType.ToDisplayString(); if (desc.RealReturnType == "bool") { desc.RealReturnType = "KalkBool"; } desc.ReturnType = method.ReturnType is INamedTypeSymbol retType && retType.IsGenericType ? "object" : desc.RealReturnType; desc.GenericCompatibleRealReturnType = method.ReturnType is IPointerTypeSymbol ? "IntPtr" : desc.RealReturnType; (desc.BaseNativeReturnType, desc.NativeReturnType) = GetBaseTypeAndType(method.ReturnType); desc.HasPointerArguments = false; for (int i = 0; i < method.Parameters.Length; i++) { var intrinsicParameter = new KalkIntrinsicParameterToGenerate(); var parameter = method.Parameters[i]; intrinsicParameter.Name = i < desc.Params.Count ? desc.Params[i].Name : parameter.Name; var parameterType = method.Parameters[i].Type; intrinsicParameter.Type = parameterType is INamedTypeSymbol paramType && paramType.IsGenericType || parameterType is IPointerTypeSymbol ? "object" : parameter.Type.ToDisplayString(); intrinsicParameter.RealType = parameter.Type.ToDisplayString(); intrinsicParameter.GenericCompatibleRealType = parameterType is IPointerTypeSymbol ? "IntPtr" : intrinsicParameter.RealType; if (parameterType is IPointerTypeSymbol) { desc.HasPointerArguments = true; } (intrinsicParameter.BaseNativeType, intrinsicParameter.NativeType) = GetBaseTypeAndType(parameter.Type); desc.Parameters.Add(intrinsicParameter); } // public object mm_add_ps(object left, object right) => ProcessArgs<float, Vector128<float>, float, Vector128<float>, float, Vector128<float>>(left, right, Sse.Add); var methodDeclaration = new StringBuilder(); methodDeclaration.Append($"public {desc.ReturnType} {desc.Name}("); for (int i = 0; i < desc.Parameters.Count; i++) { var parameter = desc.Parameters[i]; if (i > 0) { methodDeclaration.Append(", "); } methodDeclaration.Append($"{parameter.Type} {parameter.Name}"); } bool isAction = method.ReturnType.ToDisplayString() == "void"; desc.IsAction = isAction; desc.IsFunc = !desc.IsAction; methodDeclaration.Append(isAction ? $") => ProcessAction<" : $") => ({desc.ReturnType})ProcessFunc<"); var castBuilder = new StringBuilder(isAction ? "Action<" : "Func<"); for (int i = 0; i < desc.Parameters.Count; i++) { var parameter = desc.Parameters[i]; if (i > 0) { methodDeclaration.Append(", "); castBuilder.Append(", "); } methodDeclaration.Append($"{parameter.BaseNativeType}, {parameter.NativeType}"); castBuilder.Append($"{parameter.Type}"); } if (!isAction) { if (desc.Parameters.Count > 0) { methodDeclaration.Append(", "); castBuilder.Append(", "); } methodDeclaration.Append($"{desc.BaseNativeReturnType}, {desc.NativeReturnType}"); castBuilder.Append($"{desc.ReturnType}"); } methodDeclaration.Append($">("); castBuilder.Append($">"); // Gets any memory alignment RequiredAlignments.TryGetValue(intrinsicName, out int memAlign); for (int i = 0; i < desc.Parameters.Count; i++) { var parameter = desc.Parameters[i]; methodDeclaration.Append($"{parameter.Name}, "); if (memAlign > 0 && parameter.Name == "mem_addr") { methodDeclaration.Append($"{memAlign}, "); } } var finalSSEMethod = $"{method.ContainingType.ToDisplayString()}.{method.Name}"; var sseMethodName = desc.HasPointerArguments ? $"{groupName}_{method.Name}{count}" : finalSSEMethod; methodDeclaration.Append(sseMethodName); methodDeclaration.Append(");"); desc.Cast = $"({castBuilder})"; desc.MethodDeclaration = methodDeclaration.ToString(); if (desc.HasPointerArguments) { methodDeclaration.Clear(); castBuilder.Clear(); castBuilder.Append(isAction ? "Action<" : "Func<"); for (int i = 0; i < desc.Parameters.Count; i++) { var parameter = desc.Parameters[i]; if (i > 0) { castBuilder.Append(", "); } castBuilder.Append($"{parameter.GenericCompatibleRealType}"); } if (!isAction) { castBuilder.Append($", {desc.GenericCompatibleRealReturnType}"); } castBuilder.Append(">"); methodDeclaration.Append($"private unsafe readonly static {castBuilder} {sseMethodName} = new {castBuilder}(("); for (int i = 0; i < desc.Parameters.Count; i++) { if (i > 0) { methodDeclaration.Append(", "); } methodDeclaration.Append($"arg{i}"); } methodDeclaration.Append($") => {finalSSEMethod}("); for (int i = 0; i < desc.Parameters.Count; i++) { if (i > 0) { methodDeclaration.Append(", "); } var parameter = desc.Parameters[i]; methodDeclaration.Append($"({parameter.RealType})arg{i}"); } methodDeclaration.Append("));"); desc.IndirectMethodDeclaration = methodDeclaration.ToString(); } desc.Category = $"Vector Hardware Intrinsics / {docGroupName.ToUpperInvariant()}"; generatedIntrinsics.TryGetValue(desc.Name, out var existingDesc); // TODO: handle line for comments desc.Description = desc.Description.Trim().Replace("\r\n", "\n"); if (existingDesc == null || desc.Parameters[0].BaseNativeType == "float") { generatedIntrinsics[desc.Name] = desc; } desc.IsSupported = true; count++; } } Console.WriteLine($"{generatedIntrinsics.Count} intrinsics"); var intrinsics = generatedIntrinsics.Values.Where(x => x.IsSupported).ToList(); var intrinsicsPerClass = intrinsics.GroupBy(x => x.Class).ToDictionary(x => x.Key, y => y.OrderBy(x => x.Name).ToList()); var modules = new List <KalkModuleToGenerate>(); foreach (var keyPair in intrinsicsPerClass.OrderBy(x => x.Key)) { var module = new KalkModuleToGenerate { Namespace = "Kalk.Core.Modules.HardwareIntrinsics", ClassName = $"{keyPair.Key}IntrinsicsModule", Category = keyPair.Value.First().Category }; module.Members.AddRange(keyPair.Value); modules.Add(module); } return(modules); }
static async Task Main(string[] args) { var x = typeof(System.Composition.CompositionContext).Name; var rootFolder = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, @"../../../../..")); var siteFolder = Path.Combine(rootFolder, "site"); var srcFolder = Path.Combine(rootFolder, "src"); var testsFolder = Path.Combine(srcFolder, "Kalk.Tests"); var pathToSolution = Path.Combine(srcFolder, @"Kalk.Core", "Kalk.Core.csproj"); var broResult = CSharpCompilationCapture.Build(pathToSolution); var solution = broResult.Workspace.CurrentSolution; var project = solution.Projects.First(x => x.Name == "Kalk.Core"); // Make sure that doc will be parsed project = project.WithParseOptions(project.ParseOptions.WithDocumentationMode(DocumentationMode.Parse)); // Compile the project var compilation = await project.GetCompilationAsync(); var errors = compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList(); if (errors.Count > 0) { Console.WriteLine("Compilation errors:"); foreach (var error in errors) { Console.WriteLine(error); } Console.WriteLine("Error, Exiting."); Environment.Exit(1); return; } //var kalkEngine = compilation.GetTypeByMetadataName("Kalk.Core.KalkEngine"); var mapNameToModule = new Dictionary <string, KalkModuleToGenerate>(); void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData moduleAttribute, out KalkModuleToGenerate moduleToGenerate) { if (!mapNameToModule.TryGetValue(className, out moduleToGenerate)) { moduleToGenerate = new KalkModuleToGenerate() { Namespace = typeSymbol.ContainingNamespace.ToDisplayString(), ClassName = className, }; mapNameToModule.Add(className, moduleToGenerate); if (moduleAttribute != null) { moduleToGenerate.Name = moduleAttribute.ConstructorArguments[0].Value.ToString(); moduleToGenerate.Names.Add(moduleToGenerate.Name); moduleToGenerate.Category = "Modules (e.g `import Files`)"; } else { moduleToGenerate.Name = className.Replace("Module", ""); moduleToGenerate.IsBuiltin = true; } ExtractDocumentation(typeSymbol, moduleToGenerate); } } foreach (var type in compilation.GetSymbolsWithName(x => true, SymbolFilter.Type)) { var typeSymbol = type as ITypeSymbol; if (typeSymbol == null) { continue; } var moduleAttribute = typeSymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass.Name == "KalkExportModuleAttribute"); KalkModuleToGenerate moduleToGenerate = null; if (moduleAttribute != null) { GetOrCreateModule(typeSymbol, typeSymbol.Name, moduleAttribute, out moduleToGenerate); } foreach (var member in typeSymbol.GetMembers()) { var attr = member.GetAttributes().FirstOrDefault(x => x.AttributeClass.Name == "KalkExportAttribute"); if (attr == null) { continue; } var name = attr.ConstructorArguments[0].Value.ToString(); var category = attr.ConstructorArguments[1].Value.ToString(); var containingType = member.ContainingSymbol; var className = containingType.Name; // In case the module is built-in, we still generate a module for it if (moduleToGenerate == null) { GetOrCreateModule(typeSymbol, className, moduleAttribute, out moduleToGenerate); } var method = member as IMethodSymbol; var desc = new KalkMemberToGenerate() { Name = name, XmlId = member.GetDocumentationCommentId(), Category = category, IsCommand = method != null && method.ReturnsVoid, Module = moduleToGenerate, }; desc.Names.Add(name); if (method != null) { desc.CSharpName = method.Name; var builder = new StringBuilder(); desc.IsAction = method.ReturnsVoid; desc.IsFunc = !desc.IsAction; builder.Append(desc.IsAction ? "Action" : "Func"); if (method.Parameters.Length > 0 || desc.IsFunc) { builder.Append("<"); } for (var i = 0; i < method.Parameters.Length; i++) { var parameter = method.Parameters[i]; if (i > 0) { builder.Append(", "); } builder.Append(GetTypeName(parameter.Type)); } if (desc.IsFunc) { if (method.Parameters.Length > 0) { builder.Append(", "); } builder.Append(GetTypeName(method.ReturnType)); } if (method.Parameters.Length > 0 || desc.IsFunc) { builder.Append(">"); } desc.Cast = $"({builder.ToString()})"; } else if (member is IPropertySymbol || member is IFieldSymbol) { desc.CSharpName = member.Name; desc.IsConst = true; } moduleToGenerate.Members.Add(desc); ExtractDocumentation(member, desc); } } var modules = mapNameToModule.Values.OrderBy(x => x.ClassName).ToList(); var templateStr = @"//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Date: {{ date.now }} // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ using System; {{~ func GenerateDocDescriptor(item) ~}} { var descriptor = {{ if item.IsModule }}Descriptor{{ else }}Descriptors[""{{ item.Name }}""]{{ end }}; descriptor.Category = ""{{ item.Category }}""; descriptor.Description = @""{{ item.Description | string.replace '""' '""""' }}""; descriptor.IsCommand = {{ item.IsCommand }}; {{~ for param in item.Params ~}} descriptor.Params.Add(new KalkParamDescriptor(""{{ param.Name }}"", @""{{ param.Description | string.replace '""' '""""' }}"") { IsOptional = {{ param.IsOptional }} }); {{~ end ~}} {{~ if item.Returns ~}} descriptor.Returns = @""{{ item.Returns | string.replace '""' '""""' }}""; {{~ end ~}} {{~ if item.Remarks ~}} descriptor.Remarks = @""{{ item.Remarks | string.replace '""' '""""' }}""; {{~ end ~}} {{~ if item.Example ~}} descriptor.Example = @""{{ item.Example | string.replace '""' '""""' }}""; {{~ end ~}} } {{~ end ~}} {{~ for module in modules ~}} namespace {{ module.Namespace }} { public partial class {{ module.ClassName }} { {{~ if module.Name != 'All' ~}} {{~ if module.ClassName == 'KalkEngine' ~}} protected void RegisterFunctionsAuto() {{~ else ~}} protected override void RegisterFunctionsAuto() {{~ end ~}} { {{~ for member in module.Members ~}} {{~ if member.IsConst ~}} RegisterConstant(""{{ member.Name }}"", {{ member.CSharpName }}); {{~ else if member.IsFunc ~}} RegisterFunction(""{{ member.Name }}"", {{member.Cast}}{{ member.CSharpName }}); {{~ else if member.IsAction ~}} RegisterAction(""{{ member.Name }}"", {{member.Cast}}{{ member.CSharpName }}); {{~ end ~}} {{~ end ~}} RegisterDocumentationAuto(); } {{~ end ~}} private void RegisterDocumentationAuto() { {{~ if !module.IsBuiltin GenerateDocDescriptor module end for item in module.Members GenerateDocDescriptor item end ~}} } } } {{~ end ~}} "; var template = Template.Parse(templateStr); var result = template.Render(new { modules = modules }, x => x.Name); File.WriteAllText(Path.Combine(srcFolder, "Kalk.Core/KalkEngine.generated.cs"), result); var testsTemplateStr = @"//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Date: {{ date.now }} // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ using System; using NUnit.Framework; namespace Kalk.Tests { {{~ imported = {}; for module in modules; if !(imported[module.Namespace]); imported[module.Namespace] = true; ~}} using {{ module.Namespace }}; {{~ end; end ~}} {{~ for module in modules ~}} public partial class {{ module.ClassName }}Tests : KalkTestBase { {{~ for member in module.Members ~}} {{~ if (array.size member.Tests) > 0 ~}} {{~ for test in member.Tests ~}} /// <summary> /// Test for <see cref=""{{ member.XmlId }}""/> or `{{ member.Name }}`. /// </summary> [TestCase(@""{{ test.Item1 | string.replace '""' '""""' }}"", @""{{ test.Item2 | string.replace '""' '""""' }}"", Category = ""{{ member.Category }}"")] {{~ end ~}} {{~ if module.IsBuiltin ~}} public static void Test_{{ member.Name }}(string input, string output) => AssertScript(input, output); {{~ else ~}} public static void Test_{{ member.Name }}(string input, string output) => AssertScript(input, output, ""{{module.Name}}""); {{~ end ~}} {{~ end ~}} {{~ end ~}} } {{~ end ~}} } "; var templateTests = Template.Parse(testsTemplateStr); result = templateTests.Render(new { modules = modules }, x => x.Name); File.WriteAllText(Path.Combine(testsFolder, "KalkTests.generated.cs"), result); // Prism templates var prismTemplateStr = @"Prism.languages.kalk.function = /\b(?:{{ functions | array.join '|' }})\b/; "; var prismTemplate = Template.Parse(prismTemplateStr); var allFunctionNames = modules.SelectMany(x => x.Members.Select(y => y.Name)).ToList(); // special values allFunctionNames.AddRange(new [] { "null", "true", "false" }); // modules allFunctionNames.AddRange(new[] { "All", "Csv", "Currencies", "Files", "HardwareIntrinsics", "StandardUnits", "Strings", "Web" }); allFunctionNames = allFunctionNames.OrderByDescending(x => x.Length).ThenBy(x => x).ToList(); result = prismTemplate.Render(new { functions = allFunctionNames }); File.WriteAllText(Path.Combine(siteFolder, ".lunet", "js", "prism-kalk.generated.js"), result); // Generate module site documentation foreach (var module in modules) { GenerateModuleSiteDocumentation(module, siteFolder); } // Log any errors if a member doesn't have any doc or tests int functionWithMissingDoc = 0; int functionWithMissingTests = 0; foreach (var module in modules) { foreach (var member in module.Members) { var hasNoDesc = string.IsNullOrEmpty(member.Description); var hasNoTests = member.Tests.Count == 0; if ((hasNoDesc || hasNoTests) && !module.ClassName.Contains("Intrinsics")) { // We don't log for all the matrix constructors, as they are tested separately. if (module.ClassName != "TypesModule" || !member.CSharpName.StartsWith("Create")) { if (hasNoDesc) { ++functionWithMissingDoc; } if (hasNoTests) { ++functionWithMissingTests; } Console.WriteLine($"The member {member.Name} => {module.ClassName}.{member.CSharpName} doesn't have {(hasNoTests ? "any tests" + (hasNoDesc ? " and" : "") : "")} {(hasNoDesc ? "any docs" : "")}"); } } } } Console.WriteLine($"{modules.Count} modules generated."); Console.WriteLine($"{modules.SelectMany(x => x.Members).Count()} functions generated."); Console.WriteLine($"{modules.SelectMany(x => x.Members).SelectMany(y => y.Tests).Count()} tests generated."); Console.WriteLine($"{functionWithMissingDoc} functions with missing doc."); Console.WriteLine($"{functionWithMissingTests} functions with missing tests."); }