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