import type { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint' import type { Text } from '@codemirror/state' import { lspCodeActionEvent, posToOffset, } from '@kittycad/codemirror-lsp-client' import type { EditorView } from 'codemirror' import type { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' import type { CompilationError } from '@rust/kcl-lib/bindings/CompilationError' import type { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes' import type { KclError as RustKclError } from '@rust/kcl-lib/bindings/KclError' import type { ModulePath } from '@rust/kcl-lib/bindings/ModulePath' import type { Operation } from '@rust/kcl-lib/bindings/Operation' import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange' import { defaultArtifactGraph } from '@src/lang/std/artifactGraph' import { isTopLevelModule } from '@src/lang/util' import { type ArtifactGraph } from '@src/lang/wasm' import type { BacktraceItem } from '@rust/kcl-lib/bindings/BacktraceItem' import { sourceRangeContains } from '@src/lang/sourceRange' type ExtractKind = T extends { kind: infer K } ? K : never export class KCLError extends Error { kind: ExtractKind | 'name' sourceRange: SourceRange msg: string kclBacktrace: BacktraceItem[] nonFatal: CompilationError[] operations: Operation[] artifactGraph: ArtifactGraph filenames: { [x: number]: ModulePath | undefined } defaultPlanes: DefaultPlanes | null constructor( kind: ExtractKind | 'name', msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super(`${kind}: ${msg}`) this.kind = kind this.msg = msg this.sourceRange = sourceRange this.kclBacktrace = kclBacktrace this.nonFatal = nonFatal this.operations = operations this.artifactGraph = artifactGraph this.filenames = filenames this.defaultPlanes = defaultPlanes Object.setPrototypeOf(this, KCLError.prototype) } } export class KCLLexicalError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'lexical', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLSyntaxError.prototype) } } export class KCLInternalError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'internal', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLSyntaxError.prototype) } } export class KCLSyntaxError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'syntax', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLSyntaxError.prototype) } } export class KCLSemanticError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'semantic', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLSemanticError.prototype) } } export class KCLTypeError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'type', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLTypeError.prototype) } } export class KCLIoError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'io', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLIoError.prototype) } } export class KCLUnexpectedError extends KCLError { constructor( msg: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'unexpected', msg, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLUnexpectedError.prototype) } } export class KCLValueAlreadyDefined extends KCLError { constructor( key: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'name', `Key ${key} was already defined elsewhere`, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype) } } export class KCLUndefinedValueError extends KCLError { constructor( key: string, sourceRange: SourceRange, kclBacktrace: BacktraceItem[], nonFatal: CompilationError[], operations: Operation[], artifactGraph: ArtifactGraph, filenames: { [x: number]: ModulePath | undefined }, defaultPlanes: DefaultPlanes | null ) { super( 'name', `Key ${key} has not been defined`, sourceRange, kclBacktrace, nonFatal, operations, artifactGraph, filenames, defaultPlanes ) Object.setPrototypeOf(this, KCLUndefinedValueError.prototype) } } /** Convert this UTF-16 source range offset to UTF-8 as SourceRange is always a UTF-8 */ export function toUtf8( utf16SourceRange: SourceRange, sourceCode: string ): SourceRange { const moduleId = utf16SourceRange[2] const textEncoder = new TextEncoder() const prefixUtf16 = sourceCode.slice(0, utf16SourceRange[0]) const prefixUtf8 = textEncoder.encode(prefixUtf16) const prefixLen = prefixUtf8.length const toHighlightUtf16 = sourceCode.slice( utf16SourceRange[0], utf16SourceRange[1] ) const toHighlightUtf8 = textEncoder.encode(toHighlightUtf16) const toHighlightLen = toHighlightUtf8.length return [prefixLen, prefixLen + toHighlightLen, moduleId] } /** Convert this UTF-8 source range offset to UTF-16 for display in CodeMirror, as it relies on JS-style string encoding which is UTF-16. */ export function toUtf16(utf8Offset: number, sourceCode: string): number { const sourceUtf8 = new TextEncoder().encode(sourceCode) const prefix = sourceUtf8.slice(0, utf8Offset) const backTo16 = new TextDecoder().decode(prefix) return backTo16.length } /** * Maps the lsp diagnostic to an array of KclErrors. * Currently the diagnostics are all errors, but in the future they could include lints. * */ export function lspDiagnosticsToKclErrors( doc: Text, diagnostics: LspDiagnostic[] ): KCLError[] { return diagnostics .flatMap(({ range, message }) => { const sourceRange = toUtf8( [posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0], doc.toString() ) return new KCLError( 'unexpected', message, sourceRange, [], [], [], defaultArtifactGraph(), {}, null ) }) .sort((a, b) => { const c = a.sourceRange[0] const d = b.sourceRange[0] switch (true) { case c < d: return -1 case c > d: return 1 } return 0 }) } /** * Maps the KCL errors to an array of CodeMirror diagnostics. * Currently the diagnostics are all errors, but in the future they could include lints. * */ export function kclErrorsToDiagnostics( errors: KCLError[], sourceCode: string ): CodeMirrorDiagnostic[] { let nonFatal: CodeMirrorDiagnostic[] = [] const errs = errors ?.filter((err) => isTopLevelModule(err.sourceRange)) .flatMap((err) => { const diagnostics: CodeMirrorDiagnostic[] = [] let message = err.msg if (err.kclBacktrace.length > 0) { // Show the backtrace in the error message. const backtraceLines: Array = [] for (let i = 0; i < err.kclBacktrace.length; i++) { const item = err.kclBacktrace[i] if ( i > 0 && isTopLevelModule(item.sourceRange) && !sourceRangeContains(item.sourceRange, err.sourceRange) ) { diagnostics.push({ from: toUtf16(item.sourceRange[0], sourceCode), to: toUtf16(item.sourceRange[1], sourceCode), message: 'Part of the error backtrace', severity: 'hint', }) } if (i === err.kclBacktrace.length - 1 && !item.fnName) { // The top-level doesn't have a name. break } const name = item.fnName ? `${item.fnName}()` : '(anonymous)' backtraceLines.push(name) } // If the backtrace is only one line, it's not helpful to show. if (backtraceLines.length > 1) { message += `\n\nBacktrace:\n${backtraceLines.join('\n')}` } } if (err.nonFatal.length > 0) { nonFatal = nonFatal.concat( compilationErrorsToDiagnostics(err.nonFatal, sourceCode) ) } diagnostics.push({ from: toUtf16(err.sourceRange[0], sourceCode), to: toUtf16(err.sourceRange[1], sourceCode), message, severity: 'error', }) return diagnostics }) return errs.concat(nonFatal) } export function compilationErrorsToDiagnostics( errors: CompilationError[], sourceCode: string ): CodeMirrorDiagnostic[] { return errors ?.filter((err) => isTopLevelModule(err.sourceRange)) .map((err) => { let severity: any = 'error' if (err.severity === 'Warning') { severity = 'warning' } let actions const suggestion = err.suggestion if (suggestion) { actions = [ { name: suggestion.title, apply: (view: EditorView, from: number, to: number) => { view.dispatch({ changes: { from: toUtf16(suggestion.source_range[0], sourceCode), to: toUtf16(suggestion.source_range[1], sourceCode), insert: suggestion.insert, }, annotations: [lspCodeActionEvent], }) }, }, ] } return { from: toUtf16(err.sourceRange[0], sourceCode), to: toUtf16(err.sourceRange[1], sourceCode), message: err.message, severity, actions, } }) } // Create an array of KCL Errors with a new formatting to // easily map SourceRange of an error to the filename to display in the // side bar UI. This is to indicate an error in an imported file, it isn't // the specific code mirror error interface. export function kclErrorsByFilename( errors: KCLError[] ): Map { const fileNameToError: Map = new Map() errors.forEach((error: KCLError) => { const filenames = error.filenames const sourceRange: SourceRange = error.sourceRange const fileIndex = sourceRange[2] const modulePath: ModulePath | undefined = filenames[fileIndex] if (modulePath && modulePath.type === 'Local') { let localPath = modulePath.value if (localPath) { // Build up an array of errors per file name const value = fileNameToError.get(localPath) if (!value) { fileNameToError.set(localPath, [error]) } else { value.push(error) fileNameToError.set(localPath, [error]) } } } }) return fileNameToError }