Skip to content

Commit

Permalink
Added prettifying to error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpocock committed Aug 1, 2023
1 parent f66f8be commit fdc9a09
Show file tree
Hide file tree
Showing 21 changed files with 499 additions and 30 deletions.
12 changes: 9 additions & 3 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,17 @@
"publish:ovsx": "npx ovsx publish --no-dependencies $(find . -iname *.vsix)"
},
"dependencies": {
"front-matter": "^4.0.2"
"front-matter": "^4.0.2",
"lz-string": "^1.5.0",
"prettier": "^2.8.8",
"ts-dedent": "^2.2.0",
"vscode-languageserver-types": "^3.17.3",
"vscode-uri": "^3.0.7"
},
"devDependencies": {
"@types/prettier": "^2.7.3",
"@total-typescript/error-translation-engine": "workspace:*",
"@total-typescript/tips-parser": "workspace:*",
"@types/glob": "^7.2.0",
"@types/mocha": "^9.1.0",
"@types/node": "14.x",
Expand All @@ -84,7 +91,6 @@
"eslint": "^8.46.0",
"glob": "^7.2.0",
"mocha": "^9.2.2",
"typescript": "^5.1.6",
"@total-typescript/tips-parser": "workspace:*"
"typescript": "^5.1.6"
}
}
6 changes: 5 additions & 1 deletion apps/vscode/src/bundleErrors.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"code": "2314"
},
"2322": {
"body": "I was expecting a type matching A, but instead you passed B.\n",
"body": "I was expecting a type matching '{0}', but instead you passed '{1}'.\n",
"code": "2322"
},
"2324": {
Expand Down Expand Up @@ -135,6 +135,10 @@
"body": "You've created a union type that's too complex for me to handle! 🤯 I can only represent 100,000 combinations in the same union, and you've gone over that limit.\n",
"code": "2590"
},
"2739": {
"body": "Type '{0}' is missing the following properties from type '{1}': {2}",
"code": "2739"
},
"2741": {
"body": "You haven't passed all the required properties to '{2}' - '{1}' is missing the '{0}' property\n",
"code": "2741"
Expand Down
38 changes: 38 additions & 0 deletions apps/vscode/src/components/codeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { d } from "../utils";
import { miniLine } from "./miniLine";
import { spanBreak } from "./spanBreak";

/**
* @returns markdown string that will be rendered as a code block (`supportHtml` required)
* We're using codicon here since it's the only thing that can be `inline-block`
* and have a background color in hovers due to strict sanitization of markdown on
* VSCode [code](https://github.com/microsoft/vscode/blob/735aff6d962db49423e02c2344e60d418273ae39/src/vs/base/browser/markdownRenderer.ts#L372)
*/
const codeBlock = (code: string, language: string) =>
spanBreak(d/*html*/ `
\`\`\`${language}
${code}
\`\`\`
`);

export const inlineCodeBlock = (code: string, language: string) =>
codeBlock(`${code}`, language);

export const multiLineCodeBlock = (code: string, language: string) => {
const codeLines = code.split("\n");
//this line is finding the longest line
const maxLineChars = codeLines.reduce(
(acc, curr) => (curr.length > acc ? curr.length : acc),
0
);
// codicon class align the code to the center, so we must pad it with spaces
const paddedCode = codeLines
.map((line) => line.padEnd(maxLineChars + 2))
.join("\n");

return d/*html*/ `
${miniLine}
${codeBlock(paddedCode, language)}
${miniLine}
`;
};
31 changes: 31 additions & 0 deletions apps/vscode/src/components/consts/knownErrorNumbers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Diagnostic } from "vscode-languageserver-types";

/** Should be updated from:
* https://typescript.tv/errors
* Just run this in the console:
* ```javascript
* $$('h2')
* .filter(node => node.textContent.match(/TS[0-9]{1,5}/))
* .map(node => Number(node.textContent.match(/TS([0-9]+)/)[1]))
* ```
*/
export const KNOWN_ERROR_NUMBERS: Set<Diagnostic["code"]> = new Set([
1002, 1005, 1006, 1015, 1016, 1029, 1036, 1038, 1039, 1046, 1055, 1056, 1064,
1066, 1068, 1070, 1095, 1103, 1109, 1117, 1127, 1128, 1149, 1155, 1160, 1183,
1192, 1196, 1202, 1208, 1218, 1219, 1225, 1228, 1243, 1244, 1254, 1259, 1308,
1337, 1357, 1361, 1363, 1371, 1375, 1378, 1385, 1389, 1431, 1432, 1434, 1471,
2300, 2304, 2305, 2306, 2307, 2314, 2315, 2322, 2335, 2339, 2344, 2345, 2348,
2349, 2351, 2352, 2355, 2361, 2362, 2364, 2365, 2366, 2367, 2368, 2369, 2370,
2371, 2372, 2377, 2378, 2390, 2391, 2394, 2395, 2403, 2411, 2420, 2428, 2430,
2440, 2445, 2448, 2451, 2454, 2456, 2459, 2475, 2476, 2488, 2497, 2503, 2507,
2511, 2512, 2515, 2528, 2531, 2532, 2533, 2538, 2550, 2551, 2552, 2554, 2556,
2558, 2559, 2564, 2567, 2571, 2574, 2577, 2580, 2582, 2583, 2584, 2588, 2595,
2611, 2613, 2616, 2652, 2654, 2656, 2661, 2663, 2664, 2665, 2668, 2669, 2677,
2678, 2680, 2683, 2684, 2686, 2687, 2689, 2691, 2693, 2694, 2695, 2706, 2709,
2715, 2717, 2720, 2722, 2724, 2730, 2732, 2739, 2740, 2741, 2742, 2749, 2769,
2774, 2779, 2786, 2792, 2794, 2813, 2814, 4020, 4025, 4060, 4063, 4075, 4081,
4104, 4112, 4113, 4114, 5023, 5024, 5025, 5054, 5055, 5058, 5069, 5070, 5083,
5087, 5101, 6053, 6059, 6133, 6138, 6196, 6198, 6504, 7006, 7008, 7009, 7010,
7016, 7017, 7022, 7023, 7026, 7027, 7030, 7031, 7034, 7041, 7044, 7053, 8020,
17000, 17004, 17009, 18004, 18016, 80005,
]);
4 changes: 4 additions & 0 deletions apps/vscode/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./codeBlock";
export * from "./miniLine";
export * from "./spanBreak";
export * from "./unStyledCodeBlock";
4 changes: 4 additions & 0 deletions apps/vscode/src/components/miniLine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { spanBreak } from "./spanBreak";

/** May be useful for line separations */
export const miniLine = spanBreak(/*html*/ `<p></p>`);
12 changes: 12 additions & 0 deletions apps/vscode/src/components/spanBreak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { d } from "../utils";

/**
* Since every thing in the extension hover split into spans,
* we need to close the previous span before we're opening a new one
* Note: the line breaks is important here
*/
export const spanBreak = (children: string) => d/*html*/ `
</span>
${children}
<span>
`;
10 changes: 10 additions & 0 deletions apps/vscode/src/components/unStyledCodeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { inlineCodeBlock, multiLineCodeBlock } from "./codeBlock";
import { d } from "../utils";

/**
* Code block without syntax highlighting like.
* For syntax highlighting, use {@link inlineCodeBlock} or {@link multiLineCodeBlock}
*/
export const unStyledCodeBlock = (content: string) => d/*html*/ `
\`${content}\`
`;
56 changes: 56 additions & 0 deletions apps/vscode/src/format/addMissingParentheses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { has, invert, keys, values } from "../utils";

const parentheses = {
"(": ")",
"{": "}",
"[": "]",
} as const;

const openParentheses = keys(parentheses);
const closeParentheses = values(parentheses);

export function addMissingParentheses(type: string): string {
let openStack: (typeof openParentheses)[number][] = [];
let missingClosingChars = "";

for (const char of type) {
if (has(openParentheses, char)) {
openStack.push(char);
} else if (has(closeParentheses, char)) {
if (
openStack.length === 0 ||
parentheses[openStack[openStack.length - 1]] !== char
) {
// Add the correct opening character before the current closing character
openStack.push(invert(parentheses)[char]);
} else {
openStack.pop();
}
}
}

// Add the missing closing characters at the end of the string
while (openStack.length > 0) {
const openChar = openStack.pop()!;
const closingChar = parentheses[openChar];
missingClosingChars += closingChar;
}

let validType = type;

// Close the last string if it's not closed
if ((validType.match(/\"/g) ?? []).length % 2 === 1) {
validType = validType + '..."';
}
if ((validType.match(/\'/g) ?? []).length % 2 === 1) {
validType = validType + "...'";
}

validType = (validType + missingClosingChars).replace(
// Change (param: ...) to (param) => __RETURN_TYPE__ if needed
/(\([a-zA-Z0-9]*\:.*\))/,
(p1) => `${p1} => ...`
);

return validType;
}
88 changes: 88 additions & 0 deletions apps/vscode/src/format/formatDiagnosticMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { inlineCodeBlock, unStyledCodeBlock } from "../components";
import { formatTypeBlock } from "./formatTypeBlock";

const formatTypeScriptBlock = (_: string, code: string) =>
inlineCodeBlock(code, "typescript");

const formatSimpleTypeBlock = (_: string, code: string) =>
inlineCodeBlock(code, "type");

export const formatDiagnosticMessage = (
message: string,
) =>
message
// format declare module snippet
.replaceAll(
/['“](declare module )['”](.*)['“];['”]/g,
(_: string, p1: string, p2: string) =>
formatTypeScriptBlock(_, `${p1} "${p2}"`)
)
// format missing props error
.replaceAll(
/(is missing the following properties from type )'(.*)': (.+?)(?=and|$)/g,
(_, pre, type, post) =>
`${pre}${formatTypeBlock(type)}: <ul>${post
.split(", ")
.filter(Boolean)
.map((prop: string) => `<li>${prop}</li>`)
.join("")}</ul>`
)
// Format type pairs
.replaceAll(
/(types) ['“](.*?)['”] and ['“](.*?)['”][\.]?/gi,
(_: string, p1: string, p2: string, p3: string) =>
`${formatTypeBlock(p2)} and ${formatTypeBlock(
p3,
)}`
)
// Format type annotation options
.replaceAll(
/type annotation must be ['“](.*?)['”] or ['“](.*?)['”][\.]?/gi,
(_: string, p1: string, p2: string, p3: string) =>
`${formatTypeBlock(p2)} or ${formatTypeBlock(
p3,
)}`
)
.replaceAll(
/(Overload \d of \d), ['“](.*?)['”], /gi,
(_, p1: string, p2: string) => `${p1}${formatTypeBlock(p2)}`
)
// format simple strings
.replaceAll(/^['“]"[^"]*"['”]$/g, formatTypeScriptBlock)
// Format string types
.replaceAll(
/(module|file|file name) "(.*?)"(?=[\s(.|,)])/gi,
(_, p1: string, p2: string) => formatTypeBlock(`"${p2}"`)
)
// Format types
.replaceAll(
/(type|type alias|interface|module|file|file name|method's|subtype of constraint) ['“](.*?)['“](?=[\s(.|,)])/gi,
(_, p1: string, p2: string) => formatTypeBlock(p2)
)
// Format reversed types
.replaceAll(
/(.*)['“]([^>]*)['”] (type|interface|return type|file|module|is (not )?assignable)/gi,
(_: string, p1: string, p2: string, p3: string) =>
`${p1}${formatTypeBlock(p2)} ${p3}`
)
// Format simple types that didn't captured before
.replaceAll(
/['“]((void|null|undefined|any|boolean|string|number|bigint|symbol)(\[\])?)['”]/g,
formatSimpleTypeBlock
)
// Format some typescript key words
.replaceAll(
/['“](import|export|require|in|continue|break|let|false|true|const|new|throw|await|for await|[0-9]+)( ?.*?)['”]/g,
(_: string, p1: string, p2: string) =>
formatTypeScriptBlock(_, `${p1}${p2}`)
)
// Format return values
.replaceAll(
/(return|operator) ['“](.*?)['”]/gi,
(_, p1: string, p2: string) => `${p1} ${formatTypeScriptBlock("", p2)}`
)
// Format regular code blocks
.replaceAll(
/['“]((?:(?!:\s*}).)*?)['“] (?!\s*:)/g,
(_: string, p1: string) => `${unStyledCodeBlock(p1)} `
);
74 changes: 74 additions & 0 deletions apps/vscode/src/format/formatTypeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
inlineCodeBlock,
multiLineCodeBlock,
unStyledCodeBlock,
} from "../components";
import { addMissingParentheses } from "./addMissingParentheses";
import { prettify } from "./prettify";

export function formatTypeBlock(
type: string,
) {
// Return a simple code block if it's just a parenthesis
if (type.match(/^(\[\]|\{\})$/)) {
return unStyledCodeBlock(type);
}

if (
// Skip formatting if it's a simple type
type.match(
/^((void|null|undefined|any|number|string|bigint|symbol|readonly|typeof)(\[\])?)$/
)
) {
return `${inlineCodeBlock(type, "type")}`;
}

const prettyType = prettifyType(type);

if (prettyType.includes("\n")) {
return `${multiLineCodeBlock(prettyType, "type")}`;
} else {
return `${inlineCodeBlock(prettyType, "type")}`;
}
}
/**
* Try to make type prettier with prettier
*/
export function prettifyType(
type: string,
options?: { throwOnError?: boolean }
) {
try {
// Wrap type with valid statement, format it and extract the type back
return convertToOriginalType(prettify(convertToValidType(type)));
} catch (e) {
if (options?.throwOnError) {
throw e;
}
return type;
}
}

const convertToValidType = (type: string) =>
`type x = ${type
// Add missing parentheses when the type ends with "...""
.replace(/(.*)\.\.\.$/, (_, p1) => addMissingParentheses(p1))
// Replace single parameter function destructuring because it's not a valid type
// .replaceAll(/\((\{.*\})\:/g, (_, p1) => `(param: /* ${p1} */`)
// Change `(...): return` which is invalid to `(...) => return`
.replace(/^(\(.*\)): /, (_, p1) => `${p1} =>`)
.replaceAll(/... (\d{0,}) more .../g, (_, p1) => `___${p1}MORE___`)
.replaceAll(/... (\d{0,}) more ...;/g, (_, p1) => `___MORE___: ${p1};`)
.replaceAll("...;", "___KEY___: ___THREE_DOTS___;")
.replaceAll("...", "__THREE_DOTS__")};`;

const convertToOriginalType = (type: string) =>
type
.replaceAll("___KEY___: ___THREE_DOTS___", "...;")
.replaceAll("__THREE_DOTS__", "...")
.replaceAll(/___MORE___: (\d{0,});/g, (_, p1) => `... ${p1} more ...;`)
.replaceAll(/___(\d{0,})MORE___/g, (_, p1) => `... ${p1} more ...`)
.replaceAll(/... (\d{0,}) more .../g, (_, p1) => `/* ${p1} more */`) // ... x more ... not shown sell
// .replaceAll(/\(param\: \/\* (\{ .* \}) \*\//g, (_, p1) => `(${p1}: `)
.replace(/type x =[ ]?((.|\n)*);.*/g, "$1")
.trim();
Loading

0 comments on commit fdc9a09

Please sign in to comment.