1import path from "path"; 2import fs from "fs"; 3 4/** @typedef {{ 5 category: string; 6 code: number; 7 reportsUnnecessary?: {}; 8 reportsDeprecated?: {}; 9 isEarly?: boolean; 10 elidedInCompatabilityPyramid?: boolean; 11}} DiagnosticDetails */ 12 13/** @typedef {Map<string, DiagnosticDetails>} InputDiagnosticMessageTable */ 14 15async function main() { 16 if (process.argv.length < 3) { 17 console.log("Usage:"); 18 console.log("\tnode processDiagnosticMessages.mjs <diagnostic-json-input-file>"); 19 return; 20 } 21 22 /** 23 * @param {string} fileName 24 * @param {string} contents 25 */ 26 async function writeFile(fileName, contents) { 27 const filePath = path.join(path.dirname(inputFilePath), fileName); 28 try { 29 const existingContents = await fs.promises.readFile(filePath, "utf-8"); 30 if (existingContents === contents) { 31 return; 32 } 33 } 34 catch { 35 // Just write the file. 36 } 37 38 await fs.promises.writeFile(filePath, contents, { encoding: "utf-8" }); 39 } 40 41 const inputFilePath = process.argv[2].replace(/\\/g, "/"); 42 console.log(`Reading diagnostics from ${inputFilePath}`); 43 const inputStr = await fs.promises.readFile(inputFilePath, { encoding: "utf-8" }); 44 45 /** @type {{ [key: string]: DiagnosticDetails }} */ 46 const diagnosticMessagesJson = JSON.parse(inputStr); 47 48 /** @type {InputDiagnosticMessageTable} */ 49 const diagnosticMessages = new Map(); 50 for (const key in diagnosticMessagesJson) { 51 if (Object.hasOwnProperty.call(diagnosticMessagesJson, key)) { 52 diagnosticMessages.set(key, diagnosticMessagesJson[key]); 53 } 54 } 55 56 const infoFileOutput = buildInfoFileOutput(diagnosticMessages, inputFilePath); 57 checkForUniqueCodes(diagnosticMessages); 58 await writeFile("diagnosticInformationMap.generated.ts", infoFileOutput); 59 60 const messageOutput = buildDiagnosticMessageOutput(diagnosticMessages); 61 await writeFile("diagnosticMessages.generated.json", messageOutput); 62} 63 64/** 65 * @param {InputDiagnosticMessageTable} diagnosticTable 66 */ 67function checkForUniqueCodes(diagnosticTable) { 68 /** @type {Record<number, true | undefined>} */ 69 const allCodes = []; 70 diagnosticTable.forEach(({ code }) => { 71 if (allCodes[code]) { 72 throw new Error(`Diagnostic code ${code} appears more than once.`); 73 } 74 allCodes[code] = true; 75 }); 76} 77 78/** 79 * @param {InputDiagnosticMessageTable} messageTable 80 * @param {string} inputFilePathRel 81 * @returns {string} 82 */ 83function buildInfoFileOutput(messageTable, inputFilePathRel) { 84 const result = [ 85 "// <auto-generated />", 86 `// generated from '${inputFilePathRel}'`, 87 "", 88 "import { DiagnosticCategory, DiagnosticMessage } from \"./types\";", 89 "", 90 "function diag(code: number, category: DiagnosticCategory, key: string, message: string, reportsUnnecessary?: {}, elidedInCompatabilityPyramid?: boolean, reportsDeprecated?: {}): DiagnosticMessage {", 91 " return { code, category, key, message, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated };", 92 "}", 93 "", 94 "/** @internal */", 95 "export const Diagnostics = {", 96 ]; 97 messageTable.forEach(({ code, category, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated }, name) => { 98 const propName = convertPropertyName(name); 99 const argReportsUnnecessary = reportsUnnecessary ? `, /*reportsUnnecessary*/ ${reportsUnnecessary}` : ""; 100 const argElidedInCompatabilityPyramid = elidedInCompatabilityPyramid ? `${!reportsUnnecessary ? ", /*reportsUnnecessary*/ undefined" : ""}, /*elidedInCompatabilityPyramid*/ ${elidedInCompatabilityPyramid}` : ""; 101 const argReportsDeprecated = reportsDeprecated ? `${!argElidedInCompatabilityPyramid ? ", /*reportsUnnecessary*/ undefined, /*elidedInCompatabilityPyramid*/ undefined" : ""}, /*reportsDeprecated*/ ${reportsDeprecated}` : ""; 102 103 result.push(` ${propName}: diag(${code}, DiagnosticCategory.${category}, "${createKey(propName, code)}", ${JSON.stringify(name)}${argReportsUnnecessary}${argElidedInCompatabilityPyramid}${argReportsDeprecated}),`); 104 }); 105 106 result.push("};"); 107 108 return result.join("\r\n"); 109} 110 111/** 112 * @param {InputDiagnosticMessageTable} messageTable 113 * @returns {string} 114 */ 115function buildDiagnosticMessageOutput(messageTable) { 116 /** @type {Record<string, string>} */ 117 const result = {}; 118 119 messageTable.forEach(({ code }, name) => { 120 const propName = convertPropertyName(name); 121 result[createKey(propName, code)] = name; 122 }); 123 124 return JSON.stringify(result, undefined, 2).replace(/\r?\n/g, "\r\n"); 125} 126 127/** 128 * 129 * @param {string} name 130 * @param {number} code 131 * @returns {string} 132 */ 133function createKey(name, code) { 134 return name.slice(0, 100) + "_" + code; 135} 136 137/** 138 * @param {string} origName 139 * @returns {string} 140 */ 141function convertPropertyName(origName) { 142 let result = origName.split("").map(char => { 143 if (char === "*") return "_Asterisk"; 144 if (char === "/") return "_Slash"; 145 if (char === ":") return "_Colon"; 146 return /\w/.test(char) ? char : "_"; 147 }).join(""); 148 149 // get rid of all multi-underscores 150 result = result.replace(/_+/g, "_"); 151 152 // remove any leading underscore, unless it is followed by a number. 153 result = result.replace(/^_([^\d])/, "$1"); 154 155 // get rid of all trailing underscores. 156 result = result.replace(/_$/, ""); 157 158 return result; 159} 160 161main(); 162