| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Copyright (c) Microsoft Corporation. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  |  * you may not use this file except in compliance with the License. | 
					
						
							|  |  |  |  * You may obtain a copy of the License at | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * http://www.apache.org/licenses/LICENSE-2.0
 | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  |  * distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  |  * See the License for the specific language governing permissions and | 
					
						
							|  |  |  |  * limitations under the License. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // @ts-check
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const path = require('path'); | 
					
						
							|  |  |  | const Documentation = require('./documentation'); | 
					
						
							|  |  |  | const XmlDoc = require('./xmlDocumentation') | 
					
						
							|  |  |  | const PROJECT_DIR = path.join(__dirname, '..', '..'); | 
					
						
							|  |  |  | const fs = require('fs'); | 
					
						
							|  |  |  | const { parseApi } = require('./api_parser'); | 
					
						
							|  |  |  | const { Type } = require('./documentation'); | 
					
						
							|  |  |  | const { args } = require('commander'); | 
					
						
							|  |  |  | const { EOL } = require('os'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const maxDocumentationColumnWidth = 80; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @type {Map<string, Documentation.Type>} */ | 
					
						
							|  |  |  | const additionalTypes = new Map(); // this will hold types that we discover, because of .NET specifics, like results
 | 
					
						
							|  |  |  | /** @type {Map<string, string[]>} */ | 
					
						
							|  |  |  | const enumTypes = new Map(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | let documentation; | 
					
						
							|  |  |  | /** @type {Map<string, string>} */ | 
					
						
							|  |  |  | let classNameMap; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |   const typesDir = process.argv[2] || '../generate_types/csharp/'; | 
					
						
							|  |  |  |   let checkAndMakeDir = (path) => { | 
					
						
							|  |  |  |     if (!fs.existsSync(path)) | 
					
						
							|  |  |  |       fs.mkdirSync(path, { recursive: true }); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const modelsDir = path.join(typesDir, "models"); | 
					
						
							|  |  |  |   const enumsDir = path.join(typesDir, "enums"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   checkAndMakeDir(typesDir); | 
					
						
							|  |  |  |   checkAndMakeDir(modelsDir); | 
					
						
							|  |  |  |   checkAndMakeDir(enumsDir); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api')); | 
					
						
							|  |  |  |   documentation.filterForLanguage('csharp'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   documentation.setLinkRenderer(item => { | 
					
						
							|  |  |  |     if (item.clazz) | 
					
						
							|  |  |  |       return `<see cref="${translateMemberName("interface", item.clazz.name, null)}"/>`; | 
					
						
							|  |  |  |     else if (item.member) | 
					
						
							|  |  |  |       return `<see cref="${translateMemberName("interface", item.member.clazz.name, null)}.${translateMemberName(item.member.kind, item.member.name, item.member)}"/>`; | 
					
						
							|  |  |  |     else if (item.option) | 
					
						
							|  |  |  |       return `<paramref name="${item.option}"/>`; | 
					
						
							|  |  |  |     else if (item.param) | 
					
						
							|  |  |  |       return `<paramref name="${item.param}"/>`; | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       throw new Error('Unknown link format.'); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // get the template for a class
 | 
					
						
							|  |  |  |   const template = fs.readFileSync("./templates/interface.cs", 'utf-8') | 
					
						
							|  |  |  |     .replace('[PW_TOOL_VERSION]', `${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // we have some "predefined" types, like the mixed state enum, that we can map in advance
 | 
					
						
							|  |  |  |   enumTypes.set("MixedState", ["On", "Off", "Mixed"]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // map the name to a C# friendly one (we prepend an I to denote an interface)
 | 
					
						
							|  |  |  |   classNameMap = new Map(documentation.classesArray.map(x => [x.name, translateMemberName('interface', x.name, null)])); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // map some types that we know of
 | 
					
						
							|  |  |  |   classNameMap.set('Error', 'Exception'); | 
					
						
							|  |  |  |   classNameMap.set('TimeoutError', 'TimeoutException'); | 
					
						
							|  |  |  |   classNameMap.set('EvaluationArgument', 'object'); | 
					
						
							|  |  |  |   classNameMap.set('boolean', 'bool'); | 
					
						
							|  |  |  |   classNameMap.set('Serializable', 'T'); | 
					
						
							|  |  |  |   classNameMap.set('any', 'object'); | 
					
						
							|  |  |  |   classNameMap.set('Buffer', 'byte[]'); | 
					
						
							|  |  |  |   classNameMap.set('path', 'string'); | 
					
						
							|  |  |  |   classNameMap.set('URL', 'string'); | 
					
						
							|  |  |  |   classNameMap.set('RegExp', 'Regex'); | 
					
						
							| 
									
										
										
										
											2021-03-02 01:49:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   // this are types that we don't explicility render even if we get the specs
 | 
					
						
							|  |  |  |   const ignoredTypes = ['TimeoutException']; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let writeFile = (name, out, folder) => { | 
					
						
							|  |  |  |     let content = template.replace('[CONTENT]', out.join(`${EOL}\t`)); | 
					
						
							| 
									
										
										
										
											2021-03-03 01:29:29 +08:00
										 |  |  |     fs.writeFileSync(`${path.join(folder, name)}.generated.cs`, content); | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    *  | 
					
						
							|  |  |  |    * @param {string} kind  | 
					
						
							|  |  |  |    * @param {string} name  | 
					
						
							|  |  |  |    * @param {Documentation.MarkdownNode[]} spec | 
					
						
							|  |  |  |    * @param {function(string[]): void} callback  | 
					
						
							|  |  |  |    * @param {string} folder | 
					
						
							|  |  |  |    * @param {string} extendsName | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   let innerRenderElement = (kind, name, spec, callback, folder = typesDir, extendsName = null) => { | 
					
						
							|  |  |  |     const out = []; | 
					
						
							|  |  |  |     console.log(`Generating ${name}`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (spec) | 
					
						
							|  |  |  |       out.push(...XmlDoc.renderXmlDoc(spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (extendsName === 'IEventEmitter') | 
					
						
							|  |  |  |       extendsName = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     out.push(`public ${kind} ${name}${extendsName ? ` : ${extendsName}` : ''}`); | 
					
						
							|  |  |  |     out.push('{'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     callback(out); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // we want to separate the items with a space and this is nicer, than holding 
 | 
					
						
							|  |  |  |     // an index in each iterator down the line
 | 
					
						
							|  |  |  |     const lastLine = out.pop(); | 
					
						
							|  |  |  |     if (lastLine !== '') | 
					
						
							|  |  |  |       out.push(lastLine); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     out.push('}'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     writeFile(name, out, folder); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   for (const element of documentation.classesArray) { | 
					
						
							|  |  |  |     const name = classNameMap.get(element.name); | 
					
						
							|  |  |  |     if (ignoredTypes.includes(name)) | 
					
						
							|  |  |  |       continue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     innerRenderElement('partial interface', name, element.spec, (out) => { | 
					
						
							|  |  |  |       for (const member of element.membersArray) { | 
					
						
							|  |  |  |         renderMember(member, element, out); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, typesDir, translateMemberName('interface', element.extends, null)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   additionalTypes.forEach((type, name) => | 
					
						
							|  |  |  |     innerRenderElement('class', name, null, (out) => { | 
					
						
							|  |  |  |       // TODO: consider how this could be merged with the `translateType` check
 | 
					
						
							|  |  |  |       if (type.union | 
					
						
							|  |  |  |         && type.union[0].name === 'null' | 
					
						
							|  |  |  |         && type.union.length == 2) { | 
					
						
							|  |  |  |         type = type.union[1]; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (type.name === 'Array') { | 
					
						
							|  |  |  |         throw new Error('Array at this stage is unexpected.'); | 
					
						
							|  |  |  |       } else if (type.properties) { | 
					
						
							|  |  |  |         for (const member of type.properties) { | 
					
						
							|  |  |  |           let fakeType = new Type(name, null); | 
					
						
							|  |  |  |           renderMember(member, fakeType, out); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         console.log(type); | 
					
						
							|  |  |  |         throw new Error(`Not sure what to do in this case.`); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, modelsDir)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   enumTypes.forEach((values, name) => | 
					
						
							|  |  |  |     innerRenderElement('enum', name, null, (out) => { | 
					
						
							|  |  |  |       out.push('\tUndefined = 0,'); | 
					
						
							|  |  |  |       values.forEach((v, i) => { | 
					
						
							|  |  |  |         // strip out the quotes
 | 
					
						
							|  |  |  |         v = v.replace(/[\"]/g, ``) | 
					
						
							|  |  |  |         let escapedName = v.replace(/[-]/g, ' ') | 
					
						
							|  |  |  |           .split(' ') | 
					
						
							|  |  |  |           .map(word => word[0].toUpperCase() + word.substring(1)).join(''); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         out.push(`\t[EnumMember(Value = "${v}")]`); | 
					
						
							|  |  |  |         out.push(`\t${escapedName},`); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }, enumsDir)); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {string} memberKind   | 
					
						
							|  |  |  |  * @param {string} name  | 
					
						
							|  |  |  |  * @param {Documentation.Member} member  | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function translateMemberName(memberKind, name, member = null) { | 
					
						
							|  |  |  |   if (!name) return name; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // we strip it for special chars, like @ because we might get called back with it in some special cases
 | 
					
						
							|  |  |  |   // like, when generating classes inside methods for params
 | 
					
						
							|  |  |  |   name = name.replace(/[@-]/g, ''); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (memberKind === 'argument') { | 
					
						
							|  |  |  |     if (['params', 'event'].includes(name)) { // just in case we want to add others
 | 
					
						
							|  |  |  |       return `@${name}`; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       return name; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-03-02 01:49:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   // check if there's an alias in the docs, in which case
 | 
					
						
							|  |  |  |   // we return that, otherwise, we apply our dotnet magic to it
 | 
					
						
							|  |  |  |   if (member) { | 
					
						
							|  |  |  |     if (member.alias !== name) { | 
					
						
							|  |  |  |       return member.alias; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 01:49:14 +08:00
										 |  |  |   // we sanitize some common abbreviations to ensure consistency
 | 
					
						
							|  |  |  |   name = name.replace(/(HTTP[S]?)/g, (m, g) => { | 
					
						
							|  |  |  |     return g[0].toUpperCase() + g.substring(1).toLowerCase(); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   let assumedName = name.charAt(0).toUpperCase() + name.substring(1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   switch (memberKind) { | 
					
						
							|  |  |  |     case "interface": | 
					
						
							|  |  |  |       // apply name mapping if the map exists
 | 
					
						
							|  |  |  |       let mappedName = classNameMap ? classNameMap.get(assumedName) : null; | 
					
						
							|  |  |  |       if (mappedName) | 
					
						
							|  |  |  |         return mappedName; | 
					
						
							|  |  |  |       return `I${assumedName}`; | 
					
						
							|  |  |  |     case "method": | 
					
						
							|  |  |  |       if (member && member.async) | 
					
						
							|  |  |  |         return `${assumedName}Async`; | 
					
						
							|  |  |  |       return assumedName; | 
					
						
							|  |  |  |     case "event": | 
					
						
							|  |  |  |       return `${assumedName}`; | 
					
						
							|  |  |  |     case "enum": | 
					
						
							|  |  |  |       return `${assumedName}`; | 
					
						
							|  |  |  |     default: | 
					
						
							|  |  |  |       return `${assumedName}`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  *  | 
					
						
							|  |  |  |  * @param {Documentation.Member} member  | 
					
						
							|  |  |  |  * @param {Documentation.Class|Documentation.Type} parent  | 
					
						
							|  |  |  |  * @param {string[]} out | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function renderMember(member, parent, out) { | 
					
						
							|  |  |  |   let output = line => { | 
					
						
							|  |  |  |     if (typeof (line) === 'string') | 
					
						
							|  |  |  |       out.push(`\t${line}`); | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       out.push(...line.map(x => `\t${x}`)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let name = translateMemberName(member.kind, member.name, member); | 
					
						
							|  |  |  |   if (member.kind === 'method') { | 
					
						
							|  |  |  |     renderMethod(member, parent, output, name); | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     let type = translateType(member.type, parent, t => generateNameDefault(member, name, t, parent)); | 
					
						
							|  |  |  |     if (member.kind === 'event') { | 
					
						
							|  |  |  |       if (!member.type) | 
					
						
							|  |  |  |         throw new Error(`No Event Type for ${name} in ${parent.name}`); | 
					
						
							|  |  |  |       if (member.spec) | 
					
						
							|  |  |  |         output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)/*.map(x => `\t${x}`)*/); | 
					
						
							|  |  |  |       if (parent && (classNameMap.get(parent.name) === type)) | 
					
						
							|  |  |  |         output(`event EventHandler ${name};`); // event sender will be the type, so we're fine to ignore
 | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         output(`event EventHandler<${type}> ${name};`); | 
					
						
							|  |  |  |     } else if (member.kind === 'property') { | 
					
						
							|  |  |  |       if (member.spec) | 
					
						
							|  |  |  |         output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)/*.map(x => `\t${x}`)*/); | 
					
						
							|  |  |  |       output(`${type} ${name} { get; set; }`); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       throw new Error(`Problem rendering a member: ${type} - ${name} (${member.kind})`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // we're separating each entry and removing the final blank line when rendering
 | 
					
						
							|  |  |  |   out.push(''); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  *  | 
					
						
							|  |  |  |  * @param {Documentation.Member} member  | 
					
						
							|  |  |  |  * @param {string} name  | 
					
						
							|  |  |  |  * @param {Documentation.Type} t  | 
					
						
							|  |  |  |  * @param {*} parent  | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function generateNameDefault(member, name, t, parent) { | 
					
						
							|  |  |  |   if (!t.properties | 
					
						
							|  |  |  |     && !t.templates | 
					
						
							|  |  |  |     && !t.union | 
					
						
							|  |  |  |     && t.expression === '[Object]') | 
					
						
							|  |  |  |     return 'object'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // we'd get this call for enums, primarily
 | 
					
						
							|  |  |  |   let enumName = generateEnumNameIfApplicable(member, name, t, parent); | 
					
						
							|  |  |  |   if (!enumName && member) { | 
					
						
							|  |  |  |     if (member.kind === 'method' || member.kind === 'property') { | 
					
						
							|  |  |  |       // this should be easy to name... let's call it the same as the argument (eternal optimist)
 | 
					
						
							|  |  |  |       let probableName = `${parent.name}${translateMemberName(``, name, null)}`; | 
					
						
							|  |  |  |       let probableType = additionalTypes.get(probableName); | 
					
						
							|  |  |  |       if (probableType) { | 
					
						
							|  |  |  |         // compare it with what?
 | 
					
						
							|  |  |  |         if (probableType.expression != t.expression) { | 
					
						
							|  |  |  |           throw new Error(`Non-matching types with the same name. Panic.`); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         additionalTypes.set(probableName, t); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return probableName; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (member.kind === 'event') { | 
					
						
							|  |  |  |       return `${name}Payload`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return enumName || t.name; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function generateEnumNameIfApplicable(member, name, type, parent) { | 
					
						
							|  |  |  |   if (!type.union) | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const potentialValues = type.union.filter(u => u.name.startsWith('"')); | 
					
						
							|  |  |  |   if ((potentialValues.length !== type.union.length) | 
					
						
							|  |  |  |     && !(type.union[0].name === 'null' && potentialValues.length === type.union.length - 1)) | 
					
						
							|  |  |  |     return null; // this isn't an enum, so we don't care, we let the caller generate the name
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (type && type.name) | 
					
						
							|  |  |  |     return type.name; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // our enum naming policy leaves a few bits to be desired, but it'll do for now
 | 
					
						
							|  |  |  |   // however, with the recent changes, this almost never gets called anymore
 | 
					
						
							|  |  |  |   return translateMemberName('enum', name, type); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Rendering a method is so _special_, with so many weird edge cases, that it | 
					
						
							|  |  |  |  * makes sense to put it separate from the other logic.  | 
					
						
							|  |  |  |  * @param {Documentation.Member} member  | 
					
						
							|  |  |  |  * @param {Documentation.Class|Documentation.Type} parent  | 
					
						
							|  |  |  |  * @param {Function} output | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function renderMethod(member, parent, output, name) { | 
					
						
							|  |  |  |   const typeResolve = (type) => translateType(type, parent, (t) => { | 
					
						
							|  |  |  |     return `${parent.name}${translateMemberName(member.kind, member.name, null)}Result`; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** @type {Map<string, string[]>} */ | 
					
						
							|  |  |  |   const paramDocs = new Map(); | 
					
						
							|  |  |  |   const addParamsDoc = (paramName, docs) => { | 
					
						
							|  |  |  |     if (paramName.startsWith('@')) | 
					
						
							|  |  |  |       paramName = paramName.substring(1); | 
					
						
							|  |  |  |     if (paramDocs.get(paramName)) | 
					
						
							|  |  |  |       throw new Error(`Parameter ${paramName} already exists in the docs.`); | 
					
						
							|  |  |  |     paramDocs.set(paramName, docs); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** @type {string} */ | 
					
						
							|  |  |  |   let type = null; | 
					
						
							|  |  |  |   // need to check the original one
 | 
					
						
							|  |  |  |   if (member.type.name === 'Object' || member.type.name === 'Array') { | 
					
						
							|  |  |  |     let innerType = member.type; | 
					
						
							|  |  |  |     let isArray = false; | 
					
						
							|  |  |  |     if (innerType.name === 'Array') { | 
					
						
							|  |  |  |       // we want to influence the name, but also change the object type
 | 
					
						
							|  |  |  |       innerType = member.type.templates[0]; | 
					
						
							|  |  |  |       isArray = true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (innerType.expression === '[Object]<[string], [string]>') { | 
					
						
							|  |  |  |       // do nothing, because this is handled down the road
 | 
					
						
							|  |  |  |     } else if (!isArray && !innerType.properties) { | 
					
						
							|  |  |  |       type = `dynamic`; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       type = classNameMap.get(innerType.name); | 
					
						
							|  |  |  |       if (!type) { | 
					
						
							|  |  |  |         type = typeResolve(innerType); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (isArray) | 
					
						
							|  |  |  |         type = `IReadOnlyCollection<${type}>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 01:49:14 +08:00
										 |  |  |   type = type || typeResolve(member.type); | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   // TODO: this is something that will probably go into the docs
 | 
					
						
							| 
									
										
										
										
											2021-03-02 01:49:14 +08:00
										 |  |  |   // translate simple getters into read-only properties, and simple
 | 
					
						
							|  |  |  |   // set-only methods to settable properties
 | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   if (member.args.size == 0 | 
					
						
							|  |  |  |     && type !== 'void' | 
					
						
							|  |  |  |     && !name.startsWith('Get')) { | 
					
						
							|  |  |  |     if (!member.async) { | 
					
						
							|  |  |  |       if (member.spec) | 
					
						
							|  |  |  |         output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  |       output(`${type} ${name} { get; }`); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     name = `Get${name}`; | 
					
						
							| 
									
										
										
										
											2021-03-02 01:49:14 +08:00
										 |  |  |   } else if (member.args.size == 1 | 
					
						
							|  |  |  |     && type === 'void' | 
					
						
							|  |  |  |     && name.startsWith('Set') | 
					
						
							|  |  |  |     && !member.async) { | 
					
						
							|  |  |  |     name = name.substring(3); // remove the 'Set'
 | 
					
						
							|  |  |  |     if (member.spec) | 
					
						
							|  |  |  |       output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  |     output(`${translateType(member.argsArray[0].type, parent)} ${name} { set; }`); | 
					
						
							|  |  |  |     return; | 
					
						
							| 
									
										
										
										
											2021-02-27 01:04:03 +08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // HACK: special case for generics handling!
 | 
					
						
							|  |  |  |   if (type === 'T') { | 
					
						
							|  |  |  |     name = `${name}<T>`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // adjust the return type for async methods
 | 
					
						
							|  |  |  |   if (member.async) { | 
					
						
							|  |  |  |     if (type === 'void') | 
					
						
							|  |  |  |       type = `Task`; | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       type = `Task<${type}>`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // render args
 | 
					
						
							|  |  |  |   let args = []; | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    *  | 
					
						
							|  |  |  |    * @param {string} innerArgType  | 
					
						
							|  |  |  |    * @param {string} innerArgName  | 
					
						
							|  |  |  |    * @param {Documentation.Member} argument  | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   const pushArg = (innerArgType, innerArgName, argument) => { | 
					
						
							|  |  |  |     let isEnum = enumTypes.has(innerArgType); | 
					
						
							|  |  |  |     let isNullable = ['int', 'bool', 'decimal', 'float'].includes(innerArgType); | 
					
						
							|  |  |  |     const requiredPrefix = argument.required ? "" : isNullable ? "?" : ""; | 
					
						
							|  |  |  |     const requiredSuffix = argument.required ? "" : isEnum ? " = default" : " = null"; | 
					
						
							|  |  |  |     args.push(`${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let parseArg = (/** @type {Documentation.Member} */ arg) => { | 
					
						
							|  |  |  |     if (arg.name === "options") { | 
					
						
							|  |  |  |       arg.type.properties.forEach(prop => { | 
					
						
							|  |  |  |         parseArg(prop); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (arg.type.expression === '[string]|[path]') { | 
					
						
							|  |  |  |       let argName = translateMemberName('argument', arg.name, null); | 
					
						
							|  |  |  |       pushArg("string", argName, arg); | 
					
						
							|  |  |  |       pushArg("string", `${argName}Path`, arg); | 
					
						
							|  |  |  |       if (arg.spec) { | 
					
						
							|  |  |  |         addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  |         addParamsDoc(`${argName}Path`, [`Instead of specifying <paramref name="${argName}"/>, gives the file name to load from.`]); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } else if (arg.type.expression === '[boolean]|[Array]<[string]>') { | 
					
						
							|  |  |  |       // HACK: this hurts my brain too
 | 
					
						
							|  |  |  |       // we split this into two args, one boolean, with the logical name
 | 
					
						
							|  |  |  |       let argName = translateMemberName('argument', arg.name, null); | 
					
						
							|  |  |  |       let leftArgType = translateType(arg.type.union[0], parent, (t) => { throw new Error('Not supported'); }); | 
					
						
							|  |  |  |       let rightArgType = translateType(arg.type.union[1], parent, (t) => { throw new Error('Not supported'); }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       pushArg(leftArgType, argName, arg); | 
					
						
							|  |  |  |       pushArg(rightArgType, `${argName}Values`, arg); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  |       addParamsDoc(`${argName}Values`, [`The values to take into account when <paramref name="${argName}"/> is <code>true</code>.`]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const argName = translateMemberName('argument', arg.alias || arg.name, null); | 
					
						
							|  |  |  |     const argType = translateType(arg.type, parent, (t) => generateNameDefault(member, argName, t, parent)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (argType === null && arg.type.union) { | 
					
						
							|  |  |  |       // we might have to split this into multiple arguments
 | 
					
						
							|  |  |  |       let translatedArguments = arg.type.union.map(t => translateType(t, parent, (x) => generateNameDefault(member, argName, x, parent))); | 
					
						
							|  |  |  |       if (translatedArguments.includes(null)) | 
					
						
							|  |  |  |         throw new Error('Unexpected null in translated argument types. Aborting.'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       let argDocumentation = XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth); | 
					
						
							|  |  |  |       for (const newArg of translatedArguments) { | 
					
						
							|  |  |  |         const sanitizedArgName = newArg.match(/(?<=^[\s"']*)(\w+)/g, '')[0] || newArg; | 
					
						
							|  |  |  |         const newArgName = `${argName}${sanitizedArgName[0].toUpperCase() + sanitizedArgName.substring(1)}`; | 
					
						
							|  |  |  |         pushArg(newArg, newArgName, arg); | 
					
						
							|  |  |  |         addParamsDoc(newArgName, argDocumentation); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (argName === 'timeout' && argType === 'decimal') { | 
					
						
							|  |  |  |       args.push(`int timeout = 0`); // a special argument, we ignore our convention
 | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     pushArg(argType, argName, arg); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   member.args.forEach(parseArg); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   output(XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); | 
					
						
							|  |  |  |   paramDocs.forEach((val, ind) => { | 
					
						
							|  |  |  |     if (val && val.length === 1) | 
					
						
							|  |  |  |       output(`/// <param name="${ind}">${val}</param>`); | 
					
						
							|  |  |  |     else { | 
					
						
							|  |  |  |       output(`/// <param name="${ind}">`); | 
					
						
							|  |  |  |       output(val.map(l => `/// ${l}`)); | 
					
						
							|  |  |  |       output(`/// </param>`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   output(`${type} ${name}(${args.join(', ')});`); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  *  | 
					
						
							|  |  |  |  *  @callback generateNameCallback | 
					
						
							|  |  |  |  *  @param {Documentation.Type} t | 
					
						
							|  |  |  |  *  @returns {string} | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  *  @param {Documentation.Type} type  | 
					
						
							|  |  |  |  *  @param {Documentation.Class|Documentation.Type} parent | 
					
						
							|  |  |  |  *  @param {generateNameCallback} generateNameCallback | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | function translateType(type, parent, generateNameCallback = t => t.name) { | 
					
						
							|  |  |  |   // a few special cases we can fix automatically
 | 
					
						
							|  |  |  |   if (type.expression === '[null]|[Error]') | 
					
						
							|  |  |  |     return 'void'; | 
					
						
							|  |  |  |   else if (type.expression === '[boolean]|"mixed"') | 
					
						
							|  |  |  |     return 'MixedState'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let isNullableEnum = false; | 
					
						
							|  |  |  |   if (type.union) { | 
					
						
							|  |  |  |     if (type.union[0].name === 'null') { | 
					
						
							|  |  |  |       // for dotnet, this is a nullable type
 | 
					
						
							|  |  |  |       // if the other side is a primitive type
 | 
					
						
							|  |  |  |       if (type.union.length > 2) { | 
					
						
							|  |  |  |         if (type.union.filter(x => x.name.startsWith('"')).length == type.union.length - 1) | 
					
						
							|  |  |  |           isNullableEnum = true; | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           throw new Error(`Union (${parent.name}) with null is too long.`); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         const innerTypeName = translateType(type.union[1], parent, generateNameCallback); | 
					
						
							|  |  |  |         // if type is primitive, or an enum, then it's nullable
 | 
					
						
							|  |  |  |         if (innerTypeName === 'bool' | 
					
						
							|  |  |  |           || innerTypeName === 'int') { | 
					
						
							|  |  |  |           return `${innerTypeName}?`; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // if it's not a value type, it'll be nullable by default, so we can ignore it
 | 
					
						
							|  |  |  |         return `${innerTypeName}`; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (type.union.filter(u => u.name.startsWith(`"`)).length == type.union.length | 
					
						
							|  |  |  |       || isNullableEnum) { | 
					
						
							|  |  |  |       // this is an enum
 | 
					
						
							|  |  |  |       let enumName = generateNameCallback(type); | 
					
						
							|  |  |  |       if (!enumName) | 
					
						
							|  |  |  |         throw new Error(`This was supposed to be an enum, but it failed generating a name, ${type.name} ${parent ? parent.name : ""}.`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // make sure we map the enum, or invalidate the name, in case it doesn't match well
 | 
					
						
							|  |  |  |       const potentialEnum = enumTypes.get(enumName); | 
					
						
							|  |  |  |       let enumValues = type.union.filter(x => x.name !== 'null').map(x => x.name); | 
					
						
							|  |  |  |       if (potentialEnum) { | 
					
						
							|  |  |  |         // compare values
 | 
					
						
							|  |  |  |         if (potentialEnum.join(',') !== enumValues.join(',')) { | 
					
						
							|  |  |  |           // for now, we'll merge the two enums, if they have the same name, and we'll go from there
 | 
					
						
							|  |  |  |           potentialEnum.concat(enumValues.filter(x => !potentialEnum.includes(x))); // merge & de-dupe
 | 
					
						
							|  |  |  |           // TODO: think about doing global type annotation, where we can add comments, such as this?
 | 
					
						
							|  |  |  |           enumTypes.set(enumName, potentialEnum); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         enumTypes.set(enumName, enumValues); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (isNullableEnum) | 
					
						
							|  |  |  |         return `${enumName}?`; | 
					
						
							|  |  |  |       return enumName; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (type.expression === '[string]|[Buffer]') | 
					
						
							|  |  |  |       return `byte[]`; // TODO: make sure we implement extension methods for this!
 | 
					
						
							|  |  |  |     else if (type.expression === '[string]|[float]' | 
					
						
							|  |  |  |       || type.expression === '[string]|[float]|[boolean]') { | 
					
						
							|  |  |  |       console.warn(`${type.name} should be a 'string', but was a ${type.expression}`); | 
					
						
							|  |  |  |       throw new Error(`The type ${type.name} was not marked as string, but we expect it to be.`); | 
					
						
							|  |  |  |     } else if (type.union.length == 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name) | 
					
						
							|  |  |  |       return `IEnumerable<${type.union[0].name}>`; // an example of this is [string]|[Array]<[string]>
 | 
					
						
							|  |  |  |     else if (type.union[0].name === 'path') | 
					
						
							|  |  |  |       // we don't support path, but we know it's usually an object on the other end, and we expect
 | 
					
						
							|  |  |  |       // the dotnet folks to use [NameOfTheObject].LoadFromPath(); method which we can provide separately
 | 
					
						
							|  |  |  |       return translateType(type.union[1], parent, generateNameCallback); | 
					
						
							|  |  |  |     else if (type.expression === '[float]|"raf"') | 
					
						
							|  |  |  |       return `Polling`; // hardcoded because there's no other way to denote this
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (type.name === 'Array') { | 
					
						
							|  |  |  |     if (type.templates.length != 1) | 
					
						
							|  |  |  |       throw new Error(`Array (${type.name} from ${parent.name}) has more than 1 dimension. Panic.`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     let innerType = translateType(type.templates[0], parent, generateNameCallback); | 
					
						
							|  |  |  |     return `IEnumerable<${innerType}>`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (type.name === 'Object') { | 
					
						
							|  |  |  |     // take care of some common cases
 | 
					
						
							|  |  |  |     // TODO: this can be genericized
 | 
					
						
							|  |  |  |     if (type.templates && type.templates.length == 2) { | 
					
						
							|  |  |  |       // get the inner types of both templates, and if they're strings, it's a keyvaluepair string, string, 
 | 
					
						
							|  |  |  |       let keyType = translateType(type.templates[0], parent, generateNameCallback); | 
					
						
							|  |  |  |       let valueType = translateType(type.templates[1], parent, generateNameCallback); | 
					
						
							|  |  |  |       return `IEnumerable<KeyValuePair<${keyType}, ${valueType}>>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ((type.name === 'Object') | 
					
						
							|  |  |  |       && !type.properties | 
					
						
							|  |  |  |       && !type.union) { | 
					
						
							|  |  |  |       return 'object'; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     // this is an additional type that we need to generate
 | 
					
						
							|  |  |  |     let objectName = generateNameCallback(type); | 
					
						
							|  |  |  |     if (objectName === 'Object') { | 
					
						
							|  |  |  |       throw new Error('Object unexpected'); | 
					
						
							|  |  |  |     } else if (type.name === 'Object') { | 
					
						
							|  |  |  |       registerAdditionalType(objectName, type); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return objectName; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (type.name === 'Map') { | 
					
						
							|  |  |  |     if (type.templates && type.templates.length == 2) { | 
					
						
							|  |  |  |       // we map to a dictionary
 | 
					
						
							|  |  |  |       let keyType = translateType(type.templates[0], parent, generateNameCallback); | 
					
						
							|  |  |  |       let valueType = translateType(type.templates[1], parent, generateNameCallback); | 
					
						
							|  |  |  |       return `Dictionary<${keyType}, ${valueType}>`; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       throw 'Map has invalid number of templates.'; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (type.name === 'function') { | 
					
						
							|  |  |  |     if (type.expression === '[function]' || !type.args) | 
					
						
							|  |  |  |       return 'Action'; // super simple mapping
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     let argsList = ''; | 
					
						
							|  |  |  |     if (type.args) { | 
					
						
							|  |  |  |       let translatedCallbackArguments = type.args.map(t => translateType(t, parent, generateNameCallback)); | 
					
						
							|  |  |  |       if (translatedCallbackArguments.includes(null)) | 
					
						
							|  |  |  |         throw new Error('There was an argument we could not parse. Aborting.'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       argsList = translatedCallbackArguments.join(', '); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!type.returnType) { | 
					
						
							|  |  |  |       // this is an Action
 | 
					
						
							|  |  |  |       return `Action<${argsList}>`; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       let returnType = translateType(type.returnType, parent, generateNameCallback); | 
					
						
							|  |  |  |       if (returnType == null) | 
					
						
							|  |  |  |         throw new Error('Unexpected null as return type.'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return `Func<${argsList}, ${returnType}>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // there's a chance this is a name we've already seen before, so check
 | 
					
						
							|  |  |  |   // this is also where we map known types, like boolean -> bool, etc.
 | 
					
						
							|  |  |  |   let name = classNameMap.get(type.name) || type.name; | 
					
						
							|  |  |  |   return `${name}`; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  *  | 
					
						
							|  |  |  |  * @param {string} typeName  | 
					
						
							|  |  |  |  * @param {Documentation.Type} type  | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function registerAdditionalType(typeName, type) { | 
					
						
							|  |  |  |   if (['object', 'string', 'int'].includes(typeName)) | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let potentialType = additionalTypes.get(typeName); | 
					
						
							|  |  |  |   if (potentialType) { | 
					
						
							|  |  |  |     console.log(`Type ${typeName} already exists, so skipping...`); | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   additionalTypes.set(typeName, type); | 
					
						
							|  |  |  | } |