better errors from rust to lsp for execution errors (#5526)

* better errors start

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* conversions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* miette update

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* related errrors test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* a bit better

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* regenerate other errors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add diagnostics test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Jess Frazelle
2025-02-26 19:29:59 -08:00
committed by GitHub
parent 89bc93e4cd
commit f490e7d6fe
55 changed files with 779 additions and 103 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,5 +29,5 @@
}
}
],
"kcl_version": "0.2.39"
"kcl_version": "0.2.40"
}

View File

@ -730,7 +730,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.39"
version = "0.1.40"
dependencies = [
"Inflector",
"anyhow",
@ -1724,7 +1724,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.39"
version = "0.2.40"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1791,7 +1791,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.39"
version = "0.1.40"
dependencies = [
"anyhow",
"hyper 0.14.32",

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.39"
version = "0.1.40"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.39"
version = "0.1.40"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.39"
version = "0.2.40"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -22,7 +22,7 @@ clap = { version = "4.5.27", default-features = false, optional = true, features
] }
convert_case = "0.6.0"
dashmap = "6.1.0"
derive-docs = { version = "0.1.38", path = "../derive-docs" }
derive-docs = { version = "0.1.40", path = "../derive-docs" }
dhat = { version = "0.3", optional = true }
fnv = "1.0.7"
form_urlencoded = "1.2.1"

View File

@ -13,14 +13,13 @@ use itertools::Itertools;
use serde_json::json;
use tokio::task::JoinSet;
use super::kcl_doc::{ConstData, DocData, FnData};
use crate::{
docs::{is_primitive, StdLibFn},
std::StdLib,
ExecutorContext,
};
use super::kcl_doc::{ConstData, DocData, FnData};
const TYPES_DIR: &str = "../../../docs/kcl/types";
const LANG_TOPICS: [&str; 4] = ["Types", "Modules", "Settings", "Known Issues"];

View File

@ -1,13 +1,12 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use indexmap::IndexMap;
use crate::{
execution::{ArtifactCommand, ArtifactGraph, Operation},
lsp::IntoDiagnostic,
modules::ModulePath,
modules::{ModulePath, ModuleSource},
source_range::SourceRange,
ModuleId,
};
@ -120,6 +119,7 @@ pub struct KclErrorWithOutputs {
pub artifact_commands: Vec<ArtifactCommand>,
pub artifact_graph: ArtifactGraph,
pub filenames: IndexMap<ModuleId, ModulePath>,
pub source_files: IndexMap<ModuleId, ModuleSource>,
}
impl KclErrorWithOutputs {
@ -129,6 +129,7 @@ impl KclErrorWithOutputs {
artifact_commands: Vec<ArtifactCommand>,
artifact_graph: ArtifactGraph,
filenames: IndexMap<ModuleId, ModulePath>,
source_files: IndexMap<ModuleId, ModuleSource>,
) -> Self {
Self {
error,
@ -136,6 +137,7 @@ impl KclErrorWithOutputs {
artifact_commands,
artifact_graph,
filenames,
source_files,
}
}
pub fn no_outputs(error: KclError) -> Self {
@ -145,8 +147,169 @@ impl KclErrorWithOutputs {
artifact_commands: Default::default(),
artifact_graph: Default::default(),
filenames: Default::default(),
source_files: Default::default(),
}
}
pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
let mut source_ranges = self.error.source_ranges();
// Pop off the first source range to get the filename.
let first_source_range = source_ranges
.pop()
.ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
let source = self
.source_files
.get(&first_source_range.module_id())
.cloned()
.unwrap_or(ModuleSource {
source: code.to_string(),
path: self
.filenames
.get(&first_source_range.module_id())
.ok_or_else(|| {
anyhow::anyhow!(
"Could not find filename for module id: {:?}",
first_source_range.module_id()
)
})?
.clone(),
});
let filename = source.path.to_string();
let kcl_source = source.source.to_string();
let mut related = Vec::new();
for source_range in source_ranges {
let module_id = source_range.module_id();
let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
source: code.to_string(),
path: self
.filenames
.get(&module_id)
.ok_or_else(|| anyhow::anyhow!("Could not find filename for module id: {:?}", module_id))?
.clone(),
});
let error = self.error.override_source_ranges(vec![source_range]);
let report = Report {
error,
kcl_source: source.source.to_string(),
filename: source.path.to_string(),
};
related.push(report);
}
Ok(ReportWithOutputs {
error: self,
kcl_source,
filename,
related,
})
}
}
impl IntoDiagnostic for KclErrorWithOutputs {
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
let message = self.error.get_message();
let source_ranges = self.error.source_ranges();
source_ranges
.into_iter()
.map(|source_range| {
let source = self
.source_files
.get(&source_range.module_id())
.cloned()
.unwrap_or(ModuleSource {
source: code.to_string(),
path: self.filenames.get(&source_range.module_id()).unwrap().clone(),
});
let mut filename = source.path.to_string();
if !filename.starts_with("file://") {
filename = format!("file:///{}", filename.trim_start_matches("/"));
}
let related_information = if let Ok(uri) = url::Url::parse(&filename) {
Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
location: tower_lsp::lsp_types::Location {
uri,
range: source_range.to_lsp_range(&source.source),
},
message: message.to_string(),
}])
} else {
None
};
Diagnostic {
range: source_range.to_lsp_range(code),
severity: Some(self.severity()),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("kcl".to_string()),
related_information,
message: message.clone(),
tags: None,
data: None,
}
})
.collect()
}
fn severity(&self) -> DiagnosticSeverity {
DiagnosticSeverity::ERROR
}
}
#[derive(thiserror::Error, Debug)]
#[error("{}", self.error.error.get_message())]
pub struct ReportWithOutputs {
pub error: KclErrorWithOutputs,
pub kcl_source: String,
pub filename: String,
pub related: Vec<Report>,
}
impl miette::Diagnostic for ReportWithOutputs {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let family = match self.error.error {
KclError::Lexical(_) => "Lexical",
KclError::Syntax(_) => "Syntax",
KclError::Semantic(_) => "Semantic",
KclError::ImportCycle(_) => "ImportCycle",
KclError::Type(_) => "Type",
KclError::Unimplemented(_) => "Unimplemented",
KclError::Unexpected(_) => "Unexpected",
KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
KclError::UndefinedValue(_) => "UndefinedValue",
KclError::InvalidExpression(_) => "InvalidExpression",
KclError::Engine(_) => "Engine",
KclError::Internal(_) => "Internal",
};
let error_string = format!("KCL {family} error");
Some(Box::new(error_string))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.kcl_source)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let iter = self
.error
.error
.source_ranges()
.clone()
.into_iter()
.map(miette::SourceSpan::from)
.map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
Some(Box::new(iter))
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
Some(Box::new(iter))
}
}
#[derive(thiserror::Error, Debug)]
@ -188,7 +351,7 @@ impl miette::Diagnostic for Report {
.clone()
.into_iter()
.map(miette::SourceSpan::from)
.map(|span| miette::LabeledSpan::new_with_span(None, span));
.map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
Some(Box::new(iter))
}
}
@ -311,7 +474,7 @@ impl KclError {
}
impl IntoDiagnostic for KclError {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
let message = self.get_message();
let source_ranges = self.source_ranges();
@ -322,18 +485,23 @@ impl IntoDiagnostic for KclError {
.filter(|r| r.module_id() == module_id)
.collect::<Vec<_>>();
Diagnostic {
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
let mut diagnostics = Vec::new();
for source_range in &source_ranges {
diagnostics.push(Diagnostic {
range: source_range.to_lsp_range(code),
severity: Some(self.severity()),
code: None,
// TODO: this is neat we can pass a URL to a help page here for this specific error.
code_description: None,
source: Some("kcl".to_string()),
message,
related_information: None,
message: message.clone(),
tags: None,
data: None,
});
}
diagnostics
}
fn severity(&self) -> DiagnosticSeverity {

View File

@ -342,8 +342,9 @@ impl ExecutorContext {
// Add file path string to global state even if it fails to import
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let source = resolved_path.source(&self.fs, source_range).await?;
exec_state.add_id_to_source(id, source.clone());
// TODO handle parsing errors properly
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None));
Ok(id)
@ -371,7 +372,10 @@ impl ExecutorContext {
// Add file path string to global state even if it fails to import
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let source = resolved_path.source(&self.fs, source_range).await?;
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err().unwrap();
exec_state.add_id_to_source(id, source.clone());
let parsed = crate::parsing::parse_str(&source.source, id)
.parse_errs_as_err()
.unwrap();
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None));
Ok(id)
}

View File

@ -138,6 +138,7 @@
use std::{collections::HashMap, fmt};
use anyhow::Result;
use env::Environment;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -147,7 +148,6 @@ use crate::{
execution::KclValue,
source_range::SourceRange,
};
use env::Environment;
/// The distinguished name of the return value of a function.
pub(crate) const RETURN_NAME: &str = "__return";
@ -894,9 +894,8 @@ mod env {
#[cfg(test)]
mod test {
use crate::execution::kcl_value::{FunctionSource, NumericType};
use super::*;
use crate::execution::kcl_value::{FunctionSource, NumericType};
fn sr() -> SourceRange {
SourceRange::default()

View File

@ -3,8 +3,16 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
use cache::OldAstState;
pub use cache::{bust_cache, clear_mem_cache};
pub use cad_op::Operation;
pub use geometry::*;
pub(crate) use import::{
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
};
use indexmap::IndexMap;
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
use kcmc::{
each_cmd as mcmd,
ok_response::{output::TakeSnapshot, OkModelingCmdResponse},
@ -12,8 +20,10 @@ use kcmc::{
ImageFormat, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
pub use memory::EnvironmentRef;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use state::{ExecState, IdGenerator, MetaSettings};
use crate::{
engine::EngineManager,
@ -31,17 +41,6 @@ use crate::{
CompilationError, ExecError, ExecutionKind, KclErrorWithOutputs,
};
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cache::{bust_cache, clear_mem_cache};
pub use cad_op::Operation;
pub use geometry::*;
pub(crate) use import::{
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
};
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
pub use memory::EnvironmentRef;
pub use state::{ExecState, IdGenerator, MetaSettings};
pub(crate) mod annotations;
mod artifact;
pub(crate) mod cache;
@ -728,6 +727,7 @@ impl ExecutorContext {
exec_state.global.artifact_commands.clone(),
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
)
})?;

View File

@ -5,20 +5,19 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::EnvironmentRef;
use crate::{
errors::{KclError, KclErrorDetails, Severity},
execution::{
annotations, kcl_value, memory::ProgramMemory, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId,
ExecOutcome, ExecutorSettings, KclValue, Operation, UnitAngle, UnitLen,
},
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr},
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
parsing::ast::types::Annotation,
source_range::SourceRange,
CompilationError,
};
use super::EnvironmentRef;
/// State for executing a program.
#[derive(Debug, Clone)]
pub struct ExecState {
@ -34,6 +33,8 @@ pub(super) struct GlobalState {
pub id_generator: IdGenerator,
/// Map from source file absolute path to module ID.
pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
/// Map from module ID to source file.
pub id_to_source: IndexMap<ModuleId, ModuleSource>,
/// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// Output map of UUIDs to artifacts.
@ -181,6 +182,11 @@ impl ExecState {
self.global.path_to_source_id.insert(path.clone(), id);
}
pub(super) fn add_id_to_source(&mut self, id: ModuleId, source: ModuleSource) {
debug_assert!(!self.global.id_to_source.contains_key(&id));
self.global.id_to_source.insert(id, source.clone());
}
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
debug_assert!(self.global.path_to_source_id.contains_key(&path));
let module_info = ModuleInfo { id, repr, path };
@ -227,6 +233,7 @@ impl GlobalState {
operations: Default::default(),
mod_loader: Default::default(),
errors: Default::default(),
id_to_source: Default::default(),
};
let root_id = ModuleId::default();
@ -244,6 +251,8 @@ impl GlobalState {
global
.path_to_source_id
.insert(ModulePath::Local { value: root_path }, root_id);
// Ideally we'd have a way to set the root module's source here, but
// we don't have a way to get the source from the executor settings.
global
}
}

View File

@ -67,8 +67,8 @@ impl Discovered {
}
impl IntoDiagnostic for Discovered {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
(&self).to_lsp_diagnostic(code)
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
(&self).to_lsp_diagnostics(code)
}
fn severity(&self) -> DiagnosticSeverity {
@ -77,11 +77,11 @@ impl IntoDiagnostic for Discovered {
}
impl IntoDiagnostic for &Discovered {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
let message = self.finding.title.to_owned();
let source_range = self.pos;
Diagnostic {
vec![Diagnostic {
range: source_range.to_lsp_range(code),
severity: Some(self.severity()),
code: None,
@ -92,7 +92,7 @@ impl IntoDiagnostic for &Discovered {
related_information: None,
tags: None,
data: None,
}
}]
}
fn severity(&self) -> DiagnosticSeverity {

View File

@ -642,12 +642,14 @@ impl Backend {
};
for diagnostic in diagnostics {
let d = diagnostic.to_lsp_diagnostic(&params.text);
let lsp_d = diagnostic.to_lsp_diagnostics(&params.text);
// Make sure we don't duplicate diagnostics.
for d in lsp_d {
if !items.iter().any(|x| x == &d) {
items.push(d);
}
}
}
self.diagnostics_map.insert(params.uri.to_string(), items.clone());
@ -673,7 +675,7 @@ impl Backend {
match executor_ctx.run_with_caching(ast.clone()).await {
Err(err) => {
self.add_to_diagnostics(params, &[err.error], false).await;
self.add_to_diagnostics(params, &[err], false).await;
// Since we already published the diagnostics we don't really care about the error
// string.

View File

@ -18,13 +18,13 @@ use crate::{
};
impl IntoDiagnostic for CompilationError {
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
let edit = self.suggestion.as_ref().map(|s| {
let range = s.source_range.to_lsp_range(code);
serde_json::to_value((s, range)).unwrap()
});
Diagnostic {
vec![Diagnostic {
range: self.source_range.to_lsp_range(code),
severity: Some(self.severity()),
code: None,
@ -34,7 +34,7 @@ impl IntoDiagnostic for CompilationError {
related_information: None,
tags: self.tag.to_lsp_tags(),
data: edit,
}
}]
}
fn severity(&self) -> DiagnosticSeverity {

View File

@ -3473,3 +3473,68 @@ async fn kcl_test_kcl_lsp_completions_number_literal() {
assert_eq!(completions.is_none(), true);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_kcl_lsp_multi_file_error() {
let server = kcl_lsp_server(true).await.unwrap();
let cwd = std::env::current_dir().unwrap();
let joined = cwd.join("tests/import_file_parse_error/");
// Change the current directory.
std::env::set_current_dir(joined).unwrap();
let code = std::fs::read_to_string("input.kcl").unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///input.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.clone(),
},
})
.await;
// Send diagnostics request.
let diagnostics = server
.diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///input.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
identifier: None,
previous_result_id: None,
})
.await
.unwrap();
// Check the diagnostics.
if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics {
if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics {
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
let item = diagnostics.full_document_diagnostic_report.items.first().unwrap();
assert_eq!(item.message, "syntax: Unexpected token: }");
assert_eq!(
Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
location: tower_lsp::lsp_types::Location {
uri: "file:///parse-failure.kcl".try_into().unwrap(),
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 1, character: 9 },
end: tower_lsp::lsp_types::Position { line: 2, character: 1 },
},
},
message: "syntax: Unexpected token: }".to_string(),
}]),
item.related_information
);
} else {
panic!("Expected full diagnostics");
}
} else {
panic!("Expected diagnostics");
}
}

View File

@ -35,8 +35,8 @@ pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
/// Convert an object into a [lsp_types::Diagnostic] given the
/// [TextDocumentItem]'s `.text` field.
pub trait IntoDiagnostic {
/// Convert the traited object to a [lsp_types::Diagnostic].
fn to_lsp_diagnostic(&self, text: &str) -> Diagnostic;
/// Convert the traited object to a vector of [lsp_types::Diagnostic].
fn to_lsp_diagnostics(&self, text: &str) -> Vec<Diagnostic>;
/// Get the severity of the diagnostic.
fn severity(&self) -> tower_lsp::lsp_types::DiagnosticSeverity;

View File

@ -143,17 +143,23 @@ impl ModulePath {
}
}
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<String, KclError> {
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<ModuleSource, KclError> {
match self {
ModulePath::Local { value: p } => fs.read_to_string(p, source_range).await,
ModulePath::Std { value: name } => read_std(name)
ModulePath::Local { value: p } => Ok(ModuleSource {
source: fs.read_to_string(p, source_range).await?,
path: self.clone(),
}),
ModulePath::Std { value: name } => Ok(ModuleSource {
source: read_std(name)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Cannot find standard library module to import: std::{name}."),
source_ranges: vec![source_range],
})
})
.map(str::to_owned),
.map(str::to_owned)?,
path: self.clone(),
}),
ModulePath::Main => unreachable!(),
}
}
@ -188,3 +194,9 @@ impl fmt::Display for ModulePath {
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
pub struct ModuleSource {
pub path: ModulePath,
pub source: String,
}

View File

@ -128,11 +128,14 @@ async fn execute(test_name: &str, render_to_png: bool) {
// Snapshot the KCL error with a fancy graphical report.
// This looks like a Cargo compile error, with arrows pointing
// to source code, underlines, etc.
let report = crate::errors::Report {
error: error.error,
filename: format!("{test_name}.kcl"),
kcl_source: read("input.kcl", test_name),
};
miette::set_hook(Box::new(|_| {
Box::new(miette::MietteHandlerOpts::new().show_related_errors_as_nested().build())
}))
.unwrap();
let report = error
.clone()
.into_miette_report_with_outputs(&read("input.kcl", test_name))
.unwrap();
let report = miette::Report::new(report);
if previously_passed {
eprintln!("This test case failed, but it previously passed. If this is intended, and the test should actually be failing now, please delete kcl/{ok_path_str} and other associated passing artifacts");
@ -2027,6 +2030,28 @@ mod helix_simple {
}
}
mod import_file_not_exist_error {
const TEST_NAME: &str = "import_file_not_exist_error";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME);
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod import_file_parse_error {
const TEST_NAME: &str = "import_file_parse_error";

View File

@ -657,9 +657,8 @@ impl GeometryTrait for Box<Solid> {
#[cfg(test)]
mod tests {
use crate::execution::kcl_value::NumericType;
use super::*;
use crate::execution::kcl_value::NumericType;
#[test]
fn test_array_to_point3d() {

View File

@ -4,14 +4,13 @@ use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Color, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use super::sketch::PlaneData;
use crate::{
errors::KclError,
execution::{ExecState, KclValue, Plane, PlaneType},
std::Args,
};
use super::sketch::PlaneData;
/// Offset a plane by a distance along its normal.
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let std_plane = args.get_unlabeled_kw_arg("plane")?;

View File

@ -8,5 +8,6 @@ KCL Type error
╭─[5:5]
4 │
5 │ map(f, [0, 1])
·
·
· ╰── tests/argument_error/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL Semantic error
╭─[2:8]
1 │ arr = []
2 │ fail = pop(arr)
· ───────
· ───────
· ╰── tests/array_elem_pop_empty_fail/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL UndefinedValue error
╭─[3:8]
2 │ pushedArr = pop(arr)
3 │ fail = pushedArr[2]
· ───────────
· ───────────
· ╰── tests/array_elem_pop_fail/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL UndefinedValue error
╭─[3:8]
2 │ pushedArr = push(arr, 4)
3 │ fail = arr[3]
· ─────
· ─────
· ╰── tests/array_elem_push_fail/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL UndefinedValue error
╭─[2:5]
1 │ arr = []
2 │ x = arr[0]
· ─────
· ─────
· ╰── tests/array_index_oob/input.kcl
╰────

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -7,5 +7,6 @@ KCL Semantic error
× semantic: Expected a number, but found a boolean (true/false value)
╭────
1 │ assert(3 == 3 == 3, "this should not compile")
· ─────
· ─────
· ╰── tests/comparisons_multiple/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL UndefinedValue error
╭─[22:1]
21 │ // Error, after creating meaningful output.
22 │ foo
· ─
· ─
· ╰── tests/cube_with_error/input.kcl
╰────

View File

@ -10,6 +10,7 @@ KCL ImportCycle error
╭─[2:1]
1 │ @settings(defaultLengthUnit = in)
2 │ import two from "import_cycle2.kcl"
· ──────────────────────────────────
· ──────────────────────────────────
· ╰── tests/import_cycle1/input.kcl
3 │
╰────

View File

@ -0,0 +1,284 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact commands import_file_parse_error.kcl
---
[
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.7,
"g": 0.28,
"b": 0.28,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.28,
"g": 0.7,
"b": 0.28,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "plane_set_color",
"plane_id": "[uuid]",
"color": {
"r": 0.28,
"g": 0.28,
"b": 0.7,
"a": 0.4
}
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": -1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 0.0,
"y": -1.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": -1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 100.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "set_scene_units",
"unit": "mm"
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart import_file_parse_error.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,39 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing import_file_not_exist_error.kcl
---
{
"Ok": {
"body": [
{
"end": 34,
"path": {
"type": "Kcl",
"filename": "not-exist.kcl"
},
"selector": {
"type": "List",
"items": [
{
"alias": null,
"end": 13,
"name": {
"end": 13,
"name": "hotdog",
"start": 7,
"type": "Identifier"
},
"start": 7,
"type": "ImportItem"
}
]
},
"start": 0,
"type": "ImportStatement",
"type": "ImportStatement"
}
],
"end": 35,
"start": 0
}
}

View File

@ -0,0 +1,13 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing import_file_not_exist_error.kcl
---
KCL Engine error
× engine: Failed to read file `tests/import_file_not_exist_error/not-
│ exist.kcl`: No such file or directory (os error 2)
╭────
1 │ import hotdog from "not-exist.kcl"
· ─────────────────┬────────────────
· ╰── tests/import_file_not_exist_error/input.kcl
╰────

View File

@ -0,0 +1 @@
import hotdog from "not-exist.kcl"

View File

@ -0,0 +1,5 @@
---
source: kcl/src/simulation_tests.rs
description: Operations executed import_file_parse_error.kcl
---
[]

View File

@ -5,7 +5,9 @@ description: Error from executing import_file_parse_error.kcl
KCL Syntax error
× syntax: Unexpected token: }
╭────
1import hotdog from "parse-failure.kcl"
· ─
╭─[3:1]
2 return
3 │ }
· ┬
· ╰── tests/import_file_parse_error/parse-failure.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL Semantic error
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr[1.2]
· ───────
· ───────
· ╰── tests/invalid_index_fractional/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL Semantic error
╭─[3:5]
2 │ i = -1
3 │ x = arr[i]
· ─────
· ─────
· ╰── tests/invalid_index_negative/input.kcl
╰────

View File

@ -9,5 +9,6 @@ KCL Semantic error
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr["s"]
· ───────
· ───────
· ╰── tests/invalid_index_str/input.kcl
╰────

View File

@ -9,5 +9,6 @@ KCL Semantic error
╭─[2:5]
1 │ num = 999
2 │ x = num[3]
· ─────
· ─────
· ╰── tests/invalid_member_object/input.kcl
╰────

View File

@ -9,5 +9,6 @@ KCL Semantic error
╭─[2:5]
1 │ b = true
2 │ x = b["property"]
· ────────────
· ────────────
· ╰── tests/invalid_member_object_prop/input.kcl
╰────

View File

@ -9,8 +9,21 @@ KCL Semantic error
╭─[1:7]
1 │ ╭─▶ fn add(x, y) {
2 │ │ return x + y
3 │ ─▶ }
3 │ ─▶ }
· ╰──── tests/kw_fn_too_few_args/input.kcl
4 │
5 │ three = add(x = 1)
· ─────────
· ─────────
· ╰── tests/kw_fn_too_few_args/input.kcl
╰────
╰─▶ KCL Semantic error
× semantic: This function requires a parameter y, but you haven't
│ passed it one.
╭─[1:7]
1 │ ╭─▶ fn add(x, y) {
2 │ │ return x + y
3 │ ├─▶ }
· ╰──── tests/kw_fn_too_few_args/input.kcl
4 │
╰────

View File

@ -9,8 +9,21 @@ KCL Semantic error
╭─[1:7]
1 │ ╭─▶ fn add(@x) {
2 │ │ return x + 1
3 │ ─▶ }
3 │ ─▶ }
· ╰──── tests/kw_fn_unlabeled_but_has_label/input.kcl
4 │
5 │ two = add(x = 1)
· ─────────
· ─────────
· ╰── tests/kw_fn_unlabeled_but_has_label/input.kcl
╰────
╰─▶ KCL Semantic error
× semantic: The function does declare a parameter named 'x', but this
│ parameter doesn't use a label. Try removing the `x:`
╭─[1:7]
1 │ ╭─▶ fn add(@x) {
2 │ │ return x + 1
3 │ ├─▶ }
· ╰──── tests/kw_fn_unlabeled_but_has_label/input.kcl
4 │
╰────

View File

@ -9,5 +9,6 @@ KCL Semantic error
╭─[2:7]
1 │ obj = { key = 123 }
2 │ num = obj[3]
· ─────
· ─────
· ╰── tests/non_string_key_of_object/input.kcl
╰────

View File

@ -8,5 +8,6 @@ KCL UndefinedValue error
╭─[2:5]
1 │ obj = { }
2 │ k = obj["age"]
· ─────────
· ─────────
· ╰── tests/object_prop_not_found/input.kcl
╰────

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -8,6 +8,7 @@ KCL Semantic error
╭─[6:10]
5 │
6 │ answer = %
·
·
· ╰── tests/pipe_substitution_inside_function_called_from_pipeline/input.kcl
7 │ |> f(%)
╰────

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 148 KiB