Skip to content

Commit 35fe679

Browse files
author
Orta Therox
authored
Merge pull request #1932 from microsoft/twoslash_errors
Improve the twoslash error handling code by having more constistent
2 parents 3cb94f4 + bc4a0bb commit 35fe679

File tree

12 files changed

+237
-126
lines changed

12 files changed

+237
-126
lines changed

packages/ts-twoslasher/README.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,6 @@ Turns to:
233233
> {
234234
> "completions": [
235235
> {
236-
> "name": "memory",
237-
> "kind": "property",
238-
> "kindModifiers": "declare",
239-
> "sortText": "1"
240-
> },
241-
> {
242236
> "name": "assert",
243237
> "kind": "method",
244238
> "kindModifiers": "declare",
@@ -287,12 +281,6 @@ Turns to:
287281
> "sortText": "1"
288282
> },
289283
> {
290-
> "name": "exception",
291-
> "kind": "method",
292-
> "kindModifiers": "declare",
293-
> "sortText": "1"
294-
> },
295-
> {
296284
> "name": "group",
297285
> "kind": "method",
298286
> "kindModifiers": "declare",
@@ -536,7 +524,7 @@ Turns to:
536524
> "start": 72,
537525
> "line": 2,
538526
> "character": 0,
539-
> "renderedMessage": "Type 'Record<string, string>' is not assignable to type 'Record<string, number>'.\n Index signatures are incompatible.\n Type 'string' is not assignable to type 'number'.",
527+
> "renderedMessage": "Type 'Record<string, string>' is not assignable to type 'Record<string, number>'.\n 'string' index signatures are incompatible.\n Type 'string' is not assignable to type 'number'.",
540528
> "id": "err-2322-72-1"
541529
> }
542530
> ],
@@ -754,12 +742,18 @@ Turns to:
754742
> }
755743
> var __spreadArray =
756744
> (this && this.__spreadArray) ||
757-
> function (to, from) {
758-
> for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]
759-
> return to
745+
> function (to, from, pack) {
746+
> if (pack || arguments.length === 2)
747+
> for (var i = 0, l = from.length, ar; i < l; i++) {
748+
> if (ar || !(i in from)) {
749+
> if (!ar) ar = Array.prototype.slice.call(from, 0, i)
750+
> ar[i] = from[i]
751+
> }
752+
> }
753+
> return to.concat(ar || from)
760754
> }
761755
> export function fn(arr) {
762-
> var arr2 = __spreadArray([1], __read(arr))
756+
> var arr2 = __spreadArray([1], __read(arr), false)
763757
> }
764758
> ```
765759

packages/ts-twoslasher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@typescript/twoslash",
3-
"version": "2.0.2",
3+
"version": "2.1.0",
44
"license": "MIT",
55
"author": "TypeScript team",
66
"homepage": "https://github.com/microsoft/TypeScript-Website",

packages/ts-twoslasher/src/index.ts

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@ type TS = typeof import("typescript")
1010
type CompilerOptions = import("typescript").CompilerOptions
1111
type CustomTransformers = import("typescript").CustomTransformers
1212

13-
import {
14-
parsePrimitive,
15-
cleanMarkdownEscaped,
16-
typesToExtension,
17-
stringAroundIndex,
18-
getIdentifierTextSpans,
19-
getClosestWord,
20-
} from "./utils"
13+
import { parsePrimitive, cleanMarkdownEscaped, typesToExtension, getIdentifierTextSpans, getClosestWord } from "./utils"
2114
import { validateInput, validateCodeForErrors } from "./validation"
2215

2316
import { createSystem, createVirtualTypeScriptEnvironment, createFSBackedSystem } from "@typescript/vfs"
@@ -70,6 +63,34 @@ type HighlightPosition = {
7063
line: number
7164
}
7265

66+
export class TwoslashError extends Error {
67+
public title: string
68+
public description: string
69+
public recommendation: string
70+
public code: string | undefined
71+
72+
constructor(title: string, description: string, recommendation: string, code?: string | undefined) {
73+
let message = `
74+
## ${title}
75+
76+
${description}
77+
`
78+
if (recommendation) {
79+
message += `\n${recommendation}`
80+
}
81+
82+
if (code) {
83+
message += `\n${code}`
84+
}
85+
86+
super(message)
87+
this.title = title
88+
this.description = description
89+
this.recommendation = recommendation
90+
this.code = code
91+
}
92+
}
93+
7394
function filterHighlightLines(codeLines: string[]): { highlights: HighlightPosition[]; queries: QueryPosition[] } {
7495
const highlights: HighlightPosition[] = []
7596
const queries: QueryPosition[] = []
@@ -134,7 +155,12 @@ function getOptionValueFromMap(name: string, key: string, optMap: Map<string, st
134155
log(`Get ${name} mapped option: ${key} => ${result}`)
135156
if (result === undefined) {
136157
const keys = Array.from(optMap.keys() as any)
137-
throw new Error(`Invalid value ${key} for ${name}. Allowed values: ${keys.join(",")}`)
158+
159+
throw new TwoslashError(
160+
`Invalid inline compiler value`,
161+
`Got ${key} for ${name} but it is not a supported value by the TS compiler.`,
162+
`Allowed values: ${keys.join(",")}`
163+
)
138164
}
139165
return result
140166
}
@@ -171,7 +197,11 @@ function setOption(name: string, value: string, opts: CompilerOptions, ts: TS) {
171197
}
172198
}
173199

174-
throw new Error(`No compiler setting named '${name}' exists!`)
200+
throw new TwoslashError(
201+
`Invalid inline compiler flag`,
202+
`There isn't a TypeScript compiler flag called '${name}'.`,
203+
`This is likely a typo, you can check all the compiler flags in the TSConfig reference, or check the additional Twoslash flags in the npm page for @typescript/twoslash.`
204+
)
175205
}
176206

177207
const booleanConfigRegexp = /^\/\/\s?@(\w+)$/
@@ -460,12 +490,18 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
460490
const quickInfo = ls.getQuickInfoAtPosition(filename, position)
461491

462492
// prettier-ignore
463-
let text = `Could not get LSP result: ${stringAroundIndex(env.getSourceFile(filename)!.text, position)}`
464-
let docs = undefined
493+
let text: string
494+
let docs: string | undefined
465495

466496
if (quickInfo && quickInfo.displayParts) {
467497
text = quickInfo.displayParts.map(dp => dp.text).join("")
468498
docs = quickInfo.documentation ? quickInfo.documentation.map(d => d.text).join("<br/>") : undefined
499+
} else {
500+
throw new TwoslashError(
501+
`Invalid QuickInfo query`,
502+
`The request on line ${q.line} in ${filename} for quickinfo via ^? returned no from the compiler.`,
503+
`This is likely that the x positioning is off.`
504+
)
469505
}
470506

471507
const queryResult: PartialQueryResults = {
@@ -480,9 +516,13 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
480516
}
481517

482518
case "completion": {
483-
const quickInfo = ls.getCompletionsAtPosition(filename, position - 1, {})
484-
if (!quickInfo && !handbookOptions.noErrorValidation) {
485-
throw new Error(`Twoslash: The ^| query at line ${q.line} in ${filename} did not return any completions`)
519+
const completions = ls.getCompletionsAtPosition(filename, position - 1, {})
520+
if (!completions && !handbookOptions.noErrorValidation) {
521+
throw new TwoslashError(
522+
`Invalid completion query`,
523+
`The request on line ${q.line} in ${filename} for completions via ^| returned no completions from the compiler.`,
524+
`This is likely that the positioning is off.`
525+
)
486526
}
487527

488528
const word = getClosestWord(sourceFile.text, position - 1)
@@ -491,7 +531,7 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
491531

492532
const queryResult: PartialCompletionResults = {
493533
kind: "completions",
494-
completions: quickInfo?.entries || [],
534+
completions: completions?.entries || [],
495535
completionPrefix: lastDot,
496536
line: q.line - i,
497537
offset: q.offset,
@@ -548,7 +588,13 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
548588

549589
const source = env.sys.readFile(file)!
550590
const sourceFile = env.getSourceFile(file)
551-
if (!sourceFile) throw new Error(`No sourcefile found for ${file} in twoslash`)
591+
if (!sourceFile) {
592+
throw new TwoslashError(
593+
`Could not find a TypeScript sourcefile for '${file}' in the Twoslash vfs`,
594+
`It's a little hard to provide useful advice on this error. Maybe you imported something which the compiler doesn't think is a source file?`,
595+
``
596+
)
597+
}
552598

553599
// Get all of the interesting quick info popover
554600
if (!handbookOptions.showEmit) {
@@ -618,7 +664,7 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
618664

619665
// A validator that error codes are mentioned, so we can know if something has broken in the future
620666
if (!handbookOptions.noErrorValidation && relevantErrors.length) {
621-
validateCodeForErrors(relevantErrors, handbookOptions, extension, originalCode)
667+
validateCodeForErrors(relevantErrors, handbookOptions, extension, originalCode, fsRoot)
622668
}
623669

624670
let errors: TwoSlashReturn["errors"] = []
@@ -655,7 +701,11 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
655701
if (!emitSource && !compilerOptions.outFile) {
656702
const allFiles = filenames.join(", ")
657703
// prettier-ignore
658-
throw new Error(`Cannot find the corresponding **source** file for ${emitFilename} (looking for: ${emitSourceFilename} in the vfs) - in ${allFiles}`)
704+
throw new TwoslashError(
705+
`Could not find source file to show the emit for`,
706+
`Cannot find the corresponding **source** file ${emitFilename} for completions via ^| returned no quickinfo from the compiler.`,
707+
`Looked for: ${emitSourceFilename} in the vfs - which contains: ${allFiles}`
708+
)
659709
}
660710

661711
// Allow outfile, in which case you need any file.
@@ -670,8 +720,11 @@ export function twoslasher(code: string, extension: string, options: TwoSlashOpt
670720

671721
if (!file) {
672722
const allFiles = output.outputFiles.map(o => o.name).join(", ")
673-
// prettier-ignore
674-
throw new Error(`Cannot find the file ${handbookOptions.showEmittedFile} (looking for: ${fsRoot + handbookOptions.showEmittedFile} in the vfs) - in ${allFiles}`)
723+
throw new TwoslashError(
724+
`Cannot find the output file in the Twoslash VFS`,
725+
`Looking for ${handbookOptions.showEmittedFile} in the Twoslash vfs after compiling`,
726+
`Looked for" ${fsRoot + handbookOptions.showEmittedFile} in the vfs - which contains ${allFiles}.`
727+
)
675728
}
676729

677730
code = file.text

packages/ts-twoslasher/src/utils.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { TwoslashError } from "./"
2+
13
export function escapeHtml(text: string) {
24
return text.replace(/</g, "&lt;")
35
}
@@ -31,7 +33,11 @@ export function parsePrimitive(value: string, type: string): any {
3133
return value.toLowerCase() === "true" || value.length === 0
3234
}
3335

34-
throw new Error(`Unknown primitive type ${type} with - ${value}`)
36+
throw new TwoslashError(
37+
`Unknown primitive value in compiler flag`,
38+
`The only recognized primitives are number, string and boolean. Got ${type} with ${value}.`,
39+
`This is likely a typo.`
40+
)
3541
}
3642

3743
export function cleanMarkdownEscaped(code: string) {
@@ -41,23 +47,24 @@ export function cleanMarkdownEscaped(code: string) {
4147
}
4248

4349
export function typesToExtension(types: string) {
44-
switch (types) {
45-
case "js":
46-
return "js"
47-
case "javascript":
48-
return "js"
49-
case "ts":
50-
return "ts"
51-
case "typescript":
52-
return "ts"
53-
case "tsx":
54-
return "tsx"
55-
case "jsx":
56-
return "jsx"
57-
case "jsn":
58-
return "json"
50+
const map: Record<string, string> = {
51+
js: "js",
52+
javascript: "js",
53+
ts: "ts",
54+
typescript: "ts",
55+
tsx: "tsx",
56+
jsx: "jsx",
57+
json: "json",
58+
jsn: "json",
5959
}
60-
throw new Error("Cannot handle the file extension:" + types)
60+
61+
if (map[types]) return map[types]
62+
63+
throw new TwoslashError(
64+
`Unknown TypeScript extension given to Twoslash`,
65+
`Received ${types} but Twoslash only accepts: ${Object.keys(map)} `,
66+
``
67+
)
6168
}
6269

6370
export function getIdentifierTextSpans(ts: typeof import("typescript"), sourceFile: import("typescript").SourceFile) {

0 commit comments

Comments
 (0)