microsoft/qdk
Publicmirrored fromhttps://github.com/microsoft/qdkAvailable
source/npm/qsharp/src/diagnostics.ts
105lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | import { log } from "./log.js"; |
| 5 | import type { IQSharpError, IRange } from "../lib/web/qsc_wasm.js"; |
| 6 | |
| 7 | /** |
| 8 | * Public error type for most `qsharp-lang` functions. |
| 9 | * |
| 10 | * This is typically thrown by functions that deal with compiling and |
| 11 | * running code. Contains one or more VS Code-like diagnostics. |
| 12 | */ |
| 13 | export class QdkDiagnostics extends Error { |
| 14 | constructor(public readonly diagnostics: IQSharpError[]) { |
| 15 | const message = shortMessage(diagnostics); |
| 16 | super(message); |
| 17 | this.name = "QdkDiagnostics"; |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * Wrapper around QDK WASM functions to convert exceptions to a more ergonomic type. |
| 23 | * |
| 24 | * Many of the WASM functions throw exceptions that are of type `string`, |
| 25 | * which are really just JSON-serialized `IQSharpError[]`s. |
| 26 | * |
| 27 | * This function converts those exceptions into `QdkDiagnostics` instances |
| 28 | * that properly inherit from `Error`. |
| 29 | */ |
| 30 | export async function callAndTransformExceptions<T>(fn: () => Promise<T>) { |
| 31 | try { |
| 32 | return await fn(); |
| 33 | } catch (e: unknown) { |
| 34 | const QdkDiagnostics = tryParseQdkDiagnostics(e); |
| 35 | if (QdkDiagnostics) { |
| 36 | throw QdkDiagnostics; |
| 37 | } |
| 38 | throw e; |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * If the error is a string containing JSON-serialized `IQSharpError[]`, |
| 44 | * creates a `QdkDiagnostics` instance from it. |
| 45 | */ |
| 46 | function tryParseQdkDiagnostics(e: unknown): QdkDiagnostics | undefined { |
| 47 | if (typeof e === "string") { |
| 48 | try { |
| 49 | const errors = JSON.parse(e); |
| 50 | // Check for the shape of IQSharpError[] |
| 51 | if ( |
| 52 | Array.isArray(errors) && |
| 53 | errors.length > 0 && |
| 54 | errors[0].document && |
| 55 | errors[0].diagnostic |
| 56 | ) { |
| 57 | return new QdkDiagnostics(errors); |
| 58 | } |
| 59 | } catch { |
| 60 | // Couldn't parse the error as JSON. |
| 61 | log.warn(`could not parse error string ${e}`); |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | return undefined; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Constructs a human-readable message from the first error. |
| 70 | */ |
| 71 | function shortMessage(errors: IQSharpError[]) { |
| 72 | const error = errors[0]; |
| 73 | |
| 74 | return `${error.diagnostic.message}${friendlyLocation( |
| 75 | error.document, |
| 76 | error.diagnostic.range, |
| 77 | )}`; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Constructs a human-readable location string for an error. |
| 82 | */ |
| 83 | function friendlyLocation(uriOrName: string, range: IRange) { |
| 84 | if (uriOrName === "<project>") { |
| 85 | return ""; |
| 86 | } |
| 87 | // Don't make any assumptions about the format of uriOrName, |
| 88 | // it could be a file path, a URI with an arbitrary scheme, or just a name. |
| 89 | // If it contains slashes, we assume it's a path and extract the basename. |
| 90 | // Best effort, for display only. |
| 91 | const lastSlash = Math.max( |
| 92 | uriOrName.lastIndexOf("/"), |
| 93 | uriOrName.lastIndexOf("\\"), |
| 94 | ); |
| 95 | const basename = |
| 96 | lastSlash >= 0 ? uriOrName.substring(lastSlash + 1) : uriOrName; |
| 97 | |
| 98 | // avoid printing :1:1 since that's the default if the original error didn't specify a range |
| 99 | const lineColumn = |
| 100 | range.start.line > 0 || range.start.character > 0 |
| 101 | ? `:${range.start.line + 1}:${range.start.character + 1}` |
| 102 | : ""; |
| 103 | |
| 104 | return ` at ${basename}${lineColumn}`; |
| 105 | } |
| 106 | |