1use indexmap::IndexMap;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
6
7#[cfg(feature = "artifact-graph")]
8use crate::execution::{ArtifactCommand, ArtifactGraph, Operation};
9use crate::{
10 ModuleId,
11 exec::KclValue,
12 execution::DefaultPlanes,
13 lsp::IntoDiagnostic,
14 modules::{ModulePath, ModuleSource},
15 source_range::SourceRange,
16};
17
18#[derive(thiserror::Error, Debug)]
20pub enum ExecError {
21 #[error("{0}")]
22 Kcl(#[from] Box<crate::KclErrorWithOutputs>),
23 #[error("Could not connect to engine: {0}")]
24 Connection(#[from] ConnectionError),
25 #[error("PNG snapshot could not be decoded: {0}")]
26 BadPng(String),
27 #[error("Bad export: {0}")]
28 BadExport(String),
29}
30
31impl From<KclErrorWithOutputs> for ExecError {
32 fn from(error: KclErrorWithOutputs) -> Self {
33 ExecError::Kcl(Box::new(error))
34 }
35}
36
37#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
39#[derive(Debug)]
40pub struct ExecErrorWithState {
41 pub error: ExecError,
42 pub exec_state: Option<crate::execution::ExecState>,
43}
44
45impl ExecErrorWithState {
46 #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
47 pub fn new(error: ExecError, exec_state: crate::execution::ExecState) -> Self {
48 Self {
49 error,
50 exec_state: Some(exec_state),
51 }
52 }
53}
54
55impl ExecError {
56 pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
57 let ExecError::Kcl(k) = &self else {
58 return None;
59 };
60 Some(&k.error)
61 }
62}
63
64impl From<ExecError> for ExecErrorWithState {
65 fn from(error: ExecError) -> Self {
66 Self {
67 error,
68 exec_state: None,
69 }
70 }
71}
72
73impl From<ConnectionError> for ExecErrorWithState {
74 fn from(error: ConnectionError) -> Self {
75 Self {
76 error: error.into(),
77 exec_state: None,
78 }
79 }
80}
81
82#[derive(thiserror::Error, Debug)]
84pub enum ConnectionError {
85 #[error("Could not create a Zoo client: {0}")]
86 CouldNotMakeClient(anyhow::Error),
87 #[error("Could not establish connection to engine: {0}")]
88 Establishing(anyhow::Error),
89}
90
91#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
92#[ts(export)]
93#[serde(tag = "kind", rename_all = "snake_case")]
94pub enum KclError {
95 #[error("lexical: {details:?}")]
96 Lexical { details: KclErrorDetails },
97 #[error("syntax: {details:?}")]
98 Syntax { details: KclErrorDetails },
99 #[error("semantic: {details:?}")]
100 Semantic { details: KclErrorDetails },
101 #[error("import cycle: {details:?}")]
102 ImportCycle { details: KclErrorDetails },
103 #[error("argument: {details:?}")]
104 Argument { details: KclErrorDetails },
105 #[error("type: {details:?}")]
106 Type { details: KclErrorDetails },
107 #[error("i/o: {details:?}")]
108 Io { details: KclErrorDetails },
109 #[error("unexpected: {details:?}")]
110 Unexpected { details: KclErrorDetails },
111 #[error("value already defined: {details:?}")]
112 ValueAlreadyDefined { details: KclErrorDetails },
113 #[error("undefined value: {details:?}")]
114 UndefinedValue {
115 details: KclErrorDetails,
116 name: Option<String>,
117 },
118 #[error("invalid expression: {details:?}")]
119 InvalidExpression { details: KclErrorDetails },
120 #[error("engine: {details:?}")]
121 Engine { details: KclErrorDetails },
122 #[error("internal error, please report to KittyCAD team: {details:?}")]
123 Internal { details: KclErrorDetails },
124}
125
126impl From<KclErrorWithOutputs> for KclError {
127 fn from(error: KclErrorWithOutputs) -> Self {
128 error.error
129 }
130}
131
132#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
133#[error("{error}")]
134#[ts(export)]
135#[serde(rename_all = "camelCase")]
136pub struct KclErrorWithOutputs {
137 pub error: KclError,
138 pub non_fatal: Vec<CompilationError>,
139 pub variables: IndexMap<String, KclValue>,
142 #[cfg(feature = "artifact-graph")]
143 pub operations: Vec<Operation>,
144 #[cfg(feature = "artifact-graph")]
147 pub _artifact_commands: Vec<ArtifactCommand>,
148 #[cfg(feature = "artifact-graph")]
149 pub artifact_graph: ArtifactGraph,
150 pub filenames: IndexMap<ModuleId, ModulePath>,
151 pub source_files: IndexMap<ModuleId, ModuleSource>,
152 pub default_planes: Option<DefaultPlanes>,
153}
154
155impl KclErrorWithOutputs {
156 #[allow(clippy::too_many_arguments)]
157 pub fn new(
158 error: KclError,
159 non_fatal: Vec<CompilationError>,
160 variables: IndexMap<String, KclValue>,
161 #[cfg(feature = "artifact-graph")] operations: Vec<Operation>,
162 #[cfg(feature = "artifact-graph")] artifact_commands: Vec<ArtifactCommand>,
163 #[cfg(feature = "artifact-graph")] artifact_graph: ArtifactGraph,
164 filenames: IndexMap<ModuleId, ModulePath>,
165 source_files: IndexMap<ModuleId, ModuleSource>,
166 default_planes: Option<DefaultPlanes>,
167 ) -> Self {
168 Self {
169 error,
170 non_fatal,
171 variables,
172 #[cfg(feature = "artifact-graph")]
173 operations,
174 #[cfg(feature = "artifact-graph")]
175 _artifact_commands: artifact_commands,
176 #[cfg(feature = "artifact-graph")]
177 artifact_graph,
178 filenames,
179 source_files,
180 default_planes,
181 }
182 }
183 pub fn no_outputs(error: KclError) -> Self {
184 Self {
185 error,
186 non_fatal: Default::default(),
187 variables: Default::default(),
188 #[cfg(feature = "artifact-graph")]
189 operations: Default::default(),
190 #[cfg(feature = "artifact-graph")]
191 _artifact_commands: Default::default(),
192 #[cfg(feature = "artifact-graph")]
193 artifact_graph: Default::default(),
194 filenames: Default::default(),
195 source_files: Default::default(),
196 default_planes: Default::default(),
197 }
198 }
199 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
200 let mut source_ranges = self.error.source_ranges();
201
202 let first_source_range = source_ranges
204 .pop()
205 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
206
207 let source = self
208 .source_files
209 .get(&first_source_range.module_id())
210 .cloned()
211 .unwrap_or(ModuleSource {
212 source: code.to_string(),
213 path: self
214 .filenames
215 .get(&first_source_range.module_id())
216 .cloned()
217 .unwrap_or(ModulePath::Main),
218 });
219 let filename = source.path.to_string();
220 let kcl_source = source.source.to_string();
221
222 let mut related = Vec::new();
223 for source_range in source_ranges {
224 let module_id = source_range.module_id();
225 let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
226 source: code.to_string(),
227 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
228 });
229 let error = self.error.override_source_ranges(vec![source_range]);
230 let report = Report {
231 error,
232 kcl_source: source.source.to_string(),
233 filename: source.path.to_string(),
234 };
235 related.push(report);
236 }
237
238 Ok(ReportWithOutputs {
239 error: self,
240 kcl_source,
241 filename,
242 related,
243 })
244 }
245}
246
247impl IntoDiagnostic for KclErrorWithOutputs {
248 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
249 let message = self.error.get_message();
250 let source_ranges = self.error.source_ranges();
251
252 source_ranges
253 .into_iter()
254 .map(|source_range| {
255 let source = self
256 .source_files
257 .get(&source_range.module_id())
258 .cloned()
259 .unwrap_or(ModuleSource {
260 source: code.to_string(),
261 path: self.filenames.get(&source_range.module_id()).unwrap().clone(),
262 });
263 let mut filename = source.path.to_string();
264 if !filename.starts_with("file://") {
265 filename = format!("file:///{}", filename.trim_start_matches("/"));
266 }
267
268 let related_information = if let Ok(uri) = url::Url::parse(&filename) {
269 Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
270 location: tower_lsp::lsp_types::Location {
271 uri,
272 range: source_range.to_lsp_range(&source.source),
273 },
274 message: message.to_string(),
275 }])
276 } else {
277 None
278 };
279
280 Diagnostic {
281 range: source_range.to_lsp_range(code),
282 severity: Some(self.severity()),
283 code: None,
284 code_description: None,
286 source: Some("kcl".to_string()),
287 related_information,
288 message: message.clone(),
289 tags: None,
290 data: None,
291 }
292 })
293 .collect()
294 }
295
296 fn severity(&self) -> DiagnosticSeverity {
297 DiagnosticSeverity::ERROR
298 }
299}
300
301#[derive(thiserror::Error, Debug)]
302#[error("{}", self.error.error.get_message())]
303pub struct ReportWithOutputs {
304 pub error: KclErrorWithOutputs,
305 pub kcl_source: String,
306 pub filename: String,
307 pub related: Vec<Report>,
308}
309
310impl miette::Diagnostic for ReportWithOutputs {
311 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
312 let family = match self.error.error {
313 KclError::Lexical { .. } => "Lexical",
314 KclError::Syntax { .. } => "Syntax",
315 KclError::Semantic { .. } => "Semantic",
316 KclError::ImportCycle { .. } => "ImportCycle",
317 KclError::Argument { .. } => "Argument",
318 KclError::Type { .. } => "Type",
319 KclError::Io { .. } => "I/O",
320 KclError::Unexpected { .. } => "Unexpected",
321 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
322 KclError::UndefinedValue { .. } => "UndefinedValue",
323 KclError::InvalidExpression { .. } => "InvalidExpression",
324 KclError::Engine { .. } => "Engine",
325 KclError::Internal { .. } => "Internal",
326 };
327 let error_string = format!("KCL {family} error");
328 Some(Box::new(error_string))
329 }
330
331 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
332 Some(&self.kcl_source)
333 }
334
335 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
336 let iter = self
337 .error
338 .error
339 .source_ranges()
340 .clone()
341 .into_iter()
342 .map(miette::SourceSpan::from)
343 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
344 Some(Box::new(iter))
345 }
346
347 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
348 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
349 Some(Box::new(iter))
350 }
351}
352
353#[derive(thiserror::Error, Debug)]
354#[error("{}", self.error.get_message())]
355pub struct Report {
356 pub error: KclError,
357 pub kcl_source: String,
358 pub filename: String,
359}
360
361impl miette::Diagnostic for Report {
362 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
363 let family = match self.error {
364 KclError::Lexical { .. } => "Lexical",
365 KclError::Syntax { .. } => "Syntax",
366 KclError::Semantic { .. } => "Semantic",
367 KclError::ImportCycle { .. } => "ImportCycle",
368 KclError::Argument { .. } => "Argument",
369 KclError::Type { .. } => "Type",
370 KclError::Io { .. } => "I/O",
371 KclError::Unexpected { .. } => "Unexpected",
372 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
373 KclError::UndefinedValue { .. } => "UndefinedValue",
374 KclError::InvalidExpression { .. } => "InvalidExpression",
375 KclError::Engine { .. } => "Engine",
376 KclError::Internal { .. } => "Internal",
377 };
378 let error_string = format!("KCL {family} error");
379 Some(Box::new(error_string))
380 }
381
382 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
383 Some(&self.kcl_source)
384 }
385
386 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
387 let iter = self
388 .error
389 .source_ranges()
390 .clone()
391 .into_iter()
392 .map(miette::SourceSpan::from)
393 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
394 Some(Box::new(iter))
395 }
396}
397
398#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
399#[serde(rename_all = "camelCase")]
400#[error("{message}")]
401#[ts(export)]
402pub struct KclErrorDetails {
403 #[label(collection, "Errors")]
404 pub source_ranges: Vec<SourceRange>,
405 pub backtrace: Vec<BacktraceItem>,
406 #[serde(rename = "msg")]
407 pub message: String,
408}
409
410impl KclErrorDetails {
411 pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
412 let backtrace = source_ranges
413 .iter()
414 .map(|s| BacktraceItem {
415 source_range: *s,
416 fn_name: None,
417 })
418 .collect();
419 KclErrorDetails {
420 source_ranges,
421 backtrace,
422 message,
423 }
424 }
425}
426
427impl KclError {
428 pub fn internal(message: String) -> KclError {
429 KclError::Internal {
430 details: KclErrorDetails {
431 source_ranges: Default::default(),
432 backtrace: Default::default(),
433 message,
434 },
435 }
436 }
437
438 pub fn new_internal(details: KclErrorDetails) -> KclError {
439 KclError::Internal { details }
440 }
441
442 pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
443 KclError::ImportCycle { details }
444 }
445
446 pub fn new_argument(details: KclErrorDetails) -> KclError {
447 KclError::Argument { details }
448 }
449
450 pub fn new_semantic(details: KclErrorDetails) -> KclError {
451 KclError::Semantic { details }
452 }
453
454 pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
455 KclError::ValueAlreadyDefined { details }
456 }
457
458 pub fn new_syntax(details: KclErrorDetails) -> KclError {
459 KclError::Syntax { details }
460 }
461
462 pub fn new_io(details: KclErrorDetails) -> KclError {
463 KclError::Io { details }
464 }
465
466 pub fn new_engine(details: KclErrorDetails) -> KclError {
467 KclError::Engine { details }
468 }
469
470 pub fn new_lexical(details: KclErrorDetails) -> KclError {
471 KclError::Lexical { details }
472 }
473
474 pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
475 KclError::UndefinedValue { details, name }
476 }
477
478 pub fn new_type(details: KclErrorDetails) -> KclError {
479 KclError::Type { details }
480 }
481
482 pub fn get_message(&self) -> String {
484 format!("{}: {}", self.error_type(), self.message())
485 }
486
487 pub fn error_type(&self) -> &'static str {
488 match self {
489 KclError::Lexical { .. } => "lexical",
490 KclError::Syntax { .. } => "syntax",
491 KclError::Semantic { .. } => "semantic",
492 KclError::ImportCycle { .. } => "import cycle",
493 KclError::Argument { .. } => "argument",
494 KclError::Type { .. } => "type",
495 KclError::Io { .. } => "i/o",
496 KclError::Unexpected { .. } => "unexpected",
497 KclError::ValueAlreadyDefined { .. } => "value already defined",
498 KclError::UndefinedValue { .. } => "undefined value",
499 KclError::InvalidExpression { .. } => "invalid expression",
500 KclError::Engine { .. } => "engine",
501 KclError::Internal { .. } => "internal",
502 }
503 }
504
505 pub fn source_ranges(&self) -> Vec<SourceRange> {
506 match &self {
507 KclError::Lexical { details: e } => e.source_ranges.clone(),
508 KclError::Syntax { details: e } => e.source_ranges.clone(),
509 KclError::Semantic { details: e } => e.source_ranges.clone(),
510 KclError::ImportCycle { details: e } => e.source_ranges.clone(),
511 KclError::Argument { details: e } => e.source_ranges.clone(),
512 KclError::Type { details: e } => e.source_ranges.clone(),
513 KclError::Io { details: e } => e.source_ranges.clone(),
514 KclError::Unexpected { details: e } => e.source_ranges.clone(),
515 KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
516 KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
517 KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
518 KclError::Engine { details: e } => e.source_ranges.clone(),
519 KclError::Internal { details: e } => e.source_ranges.clone(),
520 }
521 }
522
523 pub fn message(&self) -> &str {
525 match &self {
526 KclError::Lexical { details: e } => &e.message,
527 KclError::Syntax { details: e } => &e.message,
528 KclError::Semantic { details: e } => &e.message,
529 KclError::ImportCycle { details: e } => &e.message,
530 KclError::Argument { details: e } => &e.message,
531 KclError::Type { details: e } => &e.message,
532 KclError::Io { details: e } => &e.message,
533 KclError::Unexpected { details: e } => &e.message,
534 KclError::ValueAlreadyDefined { details: e } => &e.message,
535 KclError::UndefinedValue { details: e, .. } => &e.message,
536 KclError::InvalidExpression { details: e } => &e.message,
537 KclError::Engine { details: e } => &e.message,
538 KclError::Internal { details: e } => &e.message,
539 }
540 }
541
542 pub fn backtrace(&self) -> Vec<BacktraceItem> {
543 match self {
544 KclError::Lexical { details: e }
545 | KclError::Syntax { details: e }
546 | KclError::Semantic { details: e }
547 | KclError::ImportCycle { details: e }
548 | KclError::Argument { details: e }
549 | KclError::Type { details: e }
550 | KclError::Io { details: e }
551 | KclError::Unexpected { details: e }
552 | KclError::ValueAlreadyDefined { details: e }
553 | KclError::UndefinedValue { details: e, .. }
554 | KclError::InvalidExpression { details: e }
555 | KclError::Engine { details: e }
556 | KclError::Internal { details: e } => e.backtrace.clone(),
557 }
558 }
559
560 pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
561 let mut new = self.clone();
562 match &mut new {
563 KclError::Lexical { details: e }
564 | KclError::Syntax { details: e }
565 | KclError::Semantic { details: e }
566 | KclError::ImportCycle { details: e }
567 | KclError::Argument { details: e }
568 | KclError::Type { details: e }
569 | KclError::Io { details: e }
570 | KclError::Unexpected { details: e }
571 | KclError::ValueAlreadyDefined { details: e }
572 | KclError::UndefinedValue { details: e, .. }
573 | KclError::InvalidExpression { details: e }
574 | KclError::Engine { details: e }
575 | KclError::Internal { details: e } => {
576 e.backtrace = source_ranges
577 .iter()
578 .map(|s| BacktraceItem {
579 source_range: *s,
580 fn_name: None,
581 })
582 .collect();
583 e.source_ranges = source_ranges;
584 }
585 }
586
587 new
588 }
589
590 pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
591 let mut new = self.clone();
592 match &mut new {
593 KclError::Lexical { details: e }
594 | KclError::Syntax { details: e }
595 | KclError::Semantic { details: e }
596 | KclError::ImportCycle { details: e }
597 | KclError::Argument { details: e }
598 | KclError::Type { details: e }
599 | KclError::Io { details: e }
600 | KclError::Unexpected { details: e }
601 | KclError::ValueAlreadyDefined { details: e }
602 | KclError::UndefinedValue { details: e, .. }
603 | KclError::InvalidExpression { details: e }
604 | KclError::Engine { details: e }
605 | KclError::Internal { details: e } => {
606 if let Some(item) = e.backtrace.last_mut() {
607 item.fn_name = last_fn_name;
608 }
609 e.backtrace.push(BacktraceItem {
610 source_range,
611 fn_name: None,
612 });
613 e.source_ranges.push(source_range);
614 }
615 }
616
617 new
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
622#[serde(rename_all = "camelCase")]
623#[ts(export)]
624pub struct BacktraceItem {
625 pub source_range: SourceRange,
626 pub fn_name: Option<String>,
627}
628
629impl std::fmt::Display for BacktraceItem {
630 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
631 if let Some(fn_name) = &self.fn_name {
632 write!(f, "{fn_name}: {:?}", self.source_range)
633 } else {
634 write!(f, "(fn): {:?}", self.source_range)
635 }
636 }
637}
638
639impl IntoDiagnostic for KclError {
640 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
641 let message = self.get_message();
642 let source_ranges = self.source_ranges();
643
644 let module_id = ModuleId::default();
646 let source_ranges = source_ranges
647 .iter()
648 .filter(|r| r.module_id() == module_id)
649 .collect::<Vec<_>>();
650
651 let mut diagnostics = Vec::new();
652 for source_range in &source_ranges {
653 diagnostics.push(Diagnostic {
654 range: source_range.to_lsp_range(code),
655 severity: Some(self.severity()),
656 code: None,
657 code_description: None,
659 source: Some("kcl".to_string()),
660 related_information: None,
661 message: message.clone(),
662 tags: None,
663 data: None,
664 });
665 }
666
667 diagnostics
668 }
669
670 fn severity(&self) -> DiagnosticSeverity {
671 DiagnosticSeverity::ERROR
672 }
673}
674
675impl From<KclError> for String {
678 fn from(error: KclError) -> Self {
679 serde_json::to_string(&error).unwrap()
680 }
681}
682
683impl From<String> for KclError {
684 fn from(error: String) -> Self {
685 serde_json::from_str(&error).unwrap()
686 }
687}
688
689#[cfg(feature = "pyo3")]
690impl From<pyo3::PyErr> for KclError {
691 fn from(error: pyo3::PyErr) -> Self {
692 KclError::new_internal(KclErrorDetails {
693 source_ranges: vec![],
694 backtrace: Default::default(),
695 message: error.to_string(),
696 })
697 }
698}
699
700#[cfg(feature = "pyo3")]
701impl From<KclError> for pyo3::PyErr {
702 fn from(error: KclError) -> Self {
703 pyo3::exceptions::PyException::new_err(error.to_string())
704 }
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS, PartialEq, Eq)]
709#[ts(export)]
710pub struct CompilationError {
711 #[serde(rename = "sourceRange")]
712 pub source_range: SourceRange,
713 pub message: String,
714 pub suggestion: Option<Suggestion>,
715 pub severity: Severity,
716 pub tag: Tag,
717}
718
719impl CompilationError {
720 pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
721 CompilationError {
722 source_range,
723 message: message.to_string(),
724 suggestion: None,
725 severity: Severity::Error,
726 tag: Tag::None,
727 }
728 }
729
730 pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
731 CompilationError {
732 source_range,
733 message: message.to_string(),
734 suggestion: None,
735 severity: Severity::Fatal,
736 tag: Tag::None,
737 }
738 }
739
740 pub(crate) fn with_suggestion(
741 self,
742 suggestion_title: impl ToString,
743 suggestion_insert: impl ToString,
744 source_range: Option<SourceRange>,
746 tag: Tag,
747 ) -> CompilationError {
748 CompilationError {
749 suggestion: Some(Suggestion {
750 title: suggestion_title.to_string(),
751 insert: suggestion_insert.to_string(),
752 source_range: source_range.unwrap_or(self.source_range),
753 }),
754 tag,
755 ..self
756 }
757 }
758
759 #[cfg(test)]
760 pub fn apply_suggestion(&self, src: &str) -> Option<String> {
761 let suggestion = self.suggestion.as_ref()?;
762 Some(format!(
763 "{}{}{}",
764 &src[0..suggestion.source_range.start()],
765 suggestion.insert,
766 &src[suggestion.source_range.end()..]
767 ))
768 }
769}
770
771impl From<CompilationError> for KclErrorDetails {
772 fn from(err: CompilationError) -> Self {
773 let backtrace = vec![BacktraceItem {
774 source_range: err.source_range,
775 fn_name: None,
776 }];
777 KclErrorDetails {
778 source_ranges: vec![err.source_range],
779 backtrace,
780 message: err.message,
781 }
782 }
783}
784
785#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
786#[ts(export)]
787pub enum Severity {
788 Warning,
789 Error,
790 Fatal,
791}
792
793impl Severity {
794 pub fn is_err(self) -> bool {
795 match self {
796 Severity::Warning => false,
797 Severity::Error | Severity::Fatal => true,
798 }
799 }
800}
801
802#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
803#[ts(export)]
804pub enum Tag {
805 Deprecated,
806 Unnecessary,
807 UnknownNumericUnits,
808 None,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS, PartialEq, Eq, JsonSchema)]
812#[ts(export)]
813pub struct Suggestion {
814 pub title: String,
815 pub insert: String,
816 pub source_range: SourceRange,
817}
818
819pub type LspSuggestion = (Suggestion, tower_lsp::lsp_types::Range);
820
821impl Suggestion {
822 pub fn to_lsp_edit(&self, code: &str) -> LspSuggestion {
823 let range = self.source_range.to_lsp_range(code);
824 (self.clone(), range)
825 }
826}