1use std::fmt;
4use std::str::FromStr;
5
6use kittycad_modeling_cmds::coord::KITTYCAD;
7use kittycad_modeling_cmds::coord::OPENGL;
8use kittycad_modeling_cmds::coord::System;
9use kittycad_modeling_cmds::coord::VULKAN;
10use serde::Deserialize;
11use serde::Serialize;
12
13use crate::KclError;
14use crate::KclVersion;
15use crate::SourceRange;
16use crate::errors::KclErrorDetails;
17use crate::errors::Severity;
18use crate::parsing::ast::types::Annotation;
19use crate::parsing::ast::types::Expr;
20use crate::parsing::ast::types::LiteralValue;
21use crate::parsing::ast::types::Node;
22use crate::parsing::ast::types::ObjectProperty;
23
24pub(super) const SIGNIFICANT_ATTRS: [&str; 4] = [SETTINGS, NO_PRELUDE, WARNINGS, DIAGNOSTICS];
26
27pub(crate) const SETTINGS: &str = "settings";
28pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
29pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
30pub(crate) const SETTINGS_VERSION: &str = "kclVersion";
31pub(crate) const SETTINGS_EXPERIMENTAL_FEATURES: &str = "experimentalFeatures";
32
33pub(super) const NO_PRELUDE: &str = "no_std";
34pub(crate) const ADDED_IN: &str = "added_in";
35pub(crate) const DEPRECATED: &str = "deprecated";
36pub(crate) const DEPRECATED_SINCE: &str = "deprecated_since";
37pub(crate) const REMOVED_IN: &str = "removed_in";
38pub(crate) const DOC_CATEGORY: &str = "doc_category";
39pub(crate) const EXPERIMENTAL: &str = "experimental";
40pub(crate) const INCLUDE_IN_FEATURE_TREE: &str = "feature_tree";
41
42pub(super) const IMPORT_FORMAT: &str = "format";
43pub(super) const IMPORT_COORDS: &str = "coords";
44pub(super) const IMPORT_COORDS_VALUES: [(&str, &System); 3] =
45 [("zoo", KITTYCAD), ("opengl", OPENGL), ("vulkan", VULKAN)];
46pub(super) const IMPORT_LENGTH_UNIT: &str = "lengthUnit";
47pub(crate) const IMPORT_TARGET_REPRESENTATION: &str = "targetRepresentation";
48
49pub(crate) const IMPL: &str = "impl";
50pub(crate) const IMPL_RUST: &str = "std_rust";
51pub(crate) const IMPL_CONSTRAINT: &str = "std_rust_constraint";
52pub(crate) const IMPL_CONSTRAINABLE: &str = "std_constrainable";
53pub(crate) const IMPL_RUST_CONSTRAINABLE: &str = "std_rust_constrainable";
54pub(crate) const IMPL_KCL: &str = "kcl";
55pub(crate) const IMPL_PRIMITIVE: &str = "primitive";
56pub(super) const IMPL_VALUES: [&str; 6] = [
57 IMPL_RUST,
58 IMPL_KCL,
59 IMPL_PRIMITIVE,
60 IMPL_CONSTRAINT,
61 IMPL_CONSTRAINABLE,
62 IMPL_RUST_CONSTRAINABLE,
63];
64
65pub(crate) const WARNINGS: &str = "warnings";
67pub(crate) const DIAGNOSTICS: &str = "diagnostics";
70pub(crate) const WARN_ALLOW: &str = "allow";
71pub(crate) const WARN_DENY: &str = "deny";
72pub(crate) const WARN_WARN: &str = "warn";
73pub(super) const WARN_LEVELS: [&str; 3] = [WARN_ALLOW, WARN_DENY, WARN_WARN];
74pub(crate) const WARN_UNKNOWN_UNITS: &str = "unknownUnits";
75pub(crate) const WARN_ANGLE_UNITS: &str = "angleUnits";
76pub(crate) const WARN_UNKNOWN_ATTR: &str = "unknownAttribute";
77pub(crate) const WARN_MOD_RETURN_VALUE: &str = "moduleReturnValue";
78pub(crate) const WARN_DEPRECATED: &str = "deprecated";
79pub(crate) const WARN_IGNORED_Z_AXIS: &str = "ignoredZAxis";
80pub(crate) const WARN_SOLVER: &str = "solver";
81pub(crate) const WARN_SHOULD_BE_PERCENTAGE: &str = "shouldBePercentage";
82pub(crate) const WARN_INVALID_MATH: &str = "invalidMath";
83pub(crate) const WARN_CSG_NO_INTERSECTION: &str = "csgNoIntersection";
84pub(crate) const WARN_UNNECESSARY_CLOSE: &str = "unnecessaryClose";
85pub(crate) const WARN_UNUSED_TAGS: &str = "unusedTags";
86pub(crate) const WARN_NOT_YET_SUPPORTED: &str = "notYetSupported";
87pub(crate) const WARN_OVER_CONSTRAINED_SKETCH: &str = "overConstrainedSketch";
88pub(crate) const WARN_REGION_LIVENESS: &str = "regionLiveness";
89pub(crate) const WARN_PARENTLESS_MERGE: &str = "parentlessMerge";
90pub(super) const WARN_VALUES: [&str; 15] = [
91 WARN_UNKNOWN_UNITS,
92 WARN_ANGLE_UNITS,
93 WARN_UNKNOWN_ATTR,
94 WARN_MOD_RETURN_VALUE,
95 WARN_DEPRECATED,
96 WARN_IGNORED_Z_AXIS,
97 WARN_SOLVER,
98 WARN_SHOULD_BE_PERCENTAGE,
99 WARN_INVALID_MATH,
100 WARN_UNNECESSARY_CLOSE,
101 WARN_NOT_YET_SUPPORTED,
102 WARN_CSG_NO_INTERSECTION,
103 WARN_OVER_CONSTRAINED_SKETCH,
104 WARN_REGION_LIVENESS,
105 WARN_PARENTLESS_MERGE,
106];
107
108#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, ts_rs::TS)]
109#[ts(export)]
110#[serde(tag = "type")]
111pub enum WarningLevel {
112 Allow,
113 Warn,
114 Deny,
115}
116
117impl WarningLevel {
118 pub(crate) fn severity(self) -> Option<Severity> {
119 match self {
120 WarningLevel::Allow => None,
121 WarningLevel::Warn => Some(Severity::Warning),
122 WarningLevel::Deny => Some(Severity::Error),
123 }
124 }
125
126 pub(crate) fn as_str(self) -> &'static str {
127 match self {
128 WarningLevel::Allow => WARN_ALLOW,
129 WarningLevel::Warn => WARN_WARN,
130 WarningLevel::Deny => WARN_DENY,
131 }
132 }
133}
134
135impl FromStr for WarningLevel {
136 type Err = ();
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 match s {
140 WARN_ALLOW => Ok(Self::Allow),
141 WARN_WARN => Ok(Self::Warn),
142 WARN_DENY => Ok(Self::Deny),
143 _ => Err(()),
144 }
145 }
146}
147
148#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
149pub enum Impl {
150 #[default]
151 Kcl,
152 KclConstrainable,
153 Rust,
154 RustConstrainable,
155 RustConstraint,
156 Primitive,
157}
158
159impl FromStr for Impl {
160 type Err = ();
161
162 fn from_str(s: &str) -> Result<Self, Self::Err> {
163 match s {
164 IMPL_RUST => Ok(Self::Rust),
165 IMPL_CONSTRAINT => Ok(Self::RustConstraint),
166 IMPL_CONSTRAINABLE => Ok(Self::KclConstrainable),
167 IMPL_RUST_CONSTRAINABLE => Ok(Self::RustConstrainable),
168 IMPL_KCL => Ok(Self::Kcl),
169 IMPL_PRIMITIVE => Ok(Self::Primitive),
170 _ => Err(()),
171 }
172 }
173}
174
175pub(crate) fn settings_completion_text() -> String {
176 format!("@{SETTINGS}({SETTINGS_UNIT_LENGTH} = mm, {SETTINGS_VERSION} = 2.0)")
177}
178
179pub(super) fn is_significant(attr: &&Node<Annotation>) -> bool {
180 match attr.name() {
181 Some(name) => SIGNIFICANT_ATTRS.contains(&name),
182 None => true,
183 }
184}
185
186pub(super) fn diagnostics_attr_name(version: KclVersion) -> &'static str {
190 if version >= KclVersion::V3Preview {
191 DIAGNOSTICS
192 } else {
193 WARNINGS
194 }
195}
196
197pub(super) fn expect_properties<'a>(
198 for_key: &'static str,
199 annotation: &'a Node<Annotation>,
200) -> Result<&'a [Node<ObjectProperty>], KclError> {
201 assert_eq!(annotation.name().unwrap(), for_key);
202 Ok(&**annotation.properties.as_ref().ok_or_else(|| {
203 KclError::new_semantic(KclErrorDetails::new(
204 format!("Empty `{for_key}` annotation"),
205 vec![annotation.as_source_range()],
206 ))
207 })?)
208}
209
210pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
211 if let Expr::Name(name) = expr
212 && let Some(name) = name.local_ident()
213 {
214 return Ok(*name);
215 }
216
217 Err(KclError::new_semantic(KclErrorDetails::new(
218 "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
219 vec![expr.into()],
220 )))
221}
222
223pub(super) fn many_of(
226 expr: &Expr,
227 of: &[&'static str],
228 attr_name: &str,
229 source_range: SourceRange,
230) -> Result<Vec<&'static str>, KclError> {
231 let unexpected_msg = format!(
232 "Unexpected {attr_name} value, expected a name or array of names, e.g., `unknownUnits` or `[unknownUnits, deprecated]`"
233 );
234
235 let values = match expr {
236 Expr::Name(name) => {
237 if let Some(name) = name.local_ident() {
238 vec![*name]
239 } else {
240 return Err(KclError::new_semantic(KclErrorDetails::new(
241 unexpected_msg,
242 vec![expr.into()],
243 )));
244 }
245 }
246 Expr::ArrayExpression(e) => {
247 let mut result = Vec::new();
248 for e in &e.elements {
249 if let Expr::Name(name) = e
250 && let Some(name) = name.local_ident()
251 {
252 result.push(*name);
253 continue;
254 }
255 return Err(KclError::new_semantic(KclErrorDetails::new(
256 unexpected_msg,
257 vec![e.into()],
258 )));
259 }
260 result
261 }
262 _ => {
263 return Err(KclError::new_semantic(KclErrorDetails::new(
264 unexpected_msg,
265 vec![expr.into()],
266 )));
267 }
268 };
269
270 let noun = attr_name.strip_suffix('s').unwrap_or(attr_name);
273 values
274 .into_iter()
275 .map(|v| {
276 of.iter()
277 .find(|vv| **vv == v)
278 .ok_or_else(|| {
279 KclError::new_semantic(KclErrorDetails::new(
280 format!("Unexpected {noun} value: `{v}`; accepted values: {}", of.join(", "),),
281 vec![source_range],
282 ))
283 })
284 .copied()
285 })
286 .collect::<Result<Vec<&str>, KclError>>()
287}
288
289pub(super) fn expect_kcl_version(expr: &Expr) -> Result<String, KclError> {
293 if let Expr::Literal(lit) = expr {
294 return match &lit.value {
295 LiteralValue::Number { .. } => Ok(lit.raw.clone()),
296 LiteralValue::String(value) => Ok(value.clone()),
297 LiteralValue::Bool(_) => Err(KclError::new_semantic(KclErrorDetails::new(
298 "Unexpected KCL version value, expected a number or string, e.g., `2.0` or `\"3.0-preview\"`"
299 .to_owned(),
300 vec![expr.into()],
301 ))),
302 };
303 }
304
305 Err(KclError::new_semantic(KclErrorDetails::new(
306 "Unexpected KCL version value, expected a number or string, e.g., `2.0` or `\"3.0-preview\"`".to_owned(),
307 vec![expr.into()],
308 )))
309}
310
311#[derive(Debug, Clone, Eq, PartialEq)]
312pub struct FnAttrs {
313 pub impl_: Impl,
314 pub deprecated: bool,
315 pub deprecated_since: Option<VersionConstraint>,
318 pub experimental: bool,
319 pub include_in_feature_tree: bool,
320}
321
322impl Default for FnAttrs {
323 fn default() -> Self {
324 Self {
325 impl_: Impl::default(),
326 deprecated: false,
327 deprecated_since: None,
328 experimental: false,
329 include_in_feature_tree: true,
330 }
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
345pub struct VersionConstraint(Vec<u32>);
346
347impl VersionConstraint {
348 pub fn parse(s: &str) -> Option<Self> {
351 let parts: Vec<u32> = s
352 .split('.')
353 .map(|p| p.parse::<u32>().ok())
354 .collect::<Option<Vec<_>>>()?;
355 if parts.is_empty() { None } else { Some(Self(parts)) }
356 }
357
358 pub(crate) fn is_before(&self, other: &Self) -> bool {
361 self.0 < other.0
362 }
363}
364
365impl fmt::Display for VersionConstraint {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 let mut first = true;
368 for n in &self.0 {
369 if !first {
370 f.write_str(".")?;
371 }
372 write!(f, "{n}")?;
373 first = false;
374 }
375 Ok(())
376 }
377}
378
379pub(crate) fn version_ge(version: &str, constraint: &VersionConstraint) -> bool {
383 let release = version.split_once('-').map_or(version, |(release, _)| release);
384 let Some(parsed) = VersionConstraint::parse(release) else {
385 return false;
386 };
387 parsed.0 >= constraint.0
388}
389
390pub(super) fn get_fn_attrs(
391 annotations: &[Node<Annotation>],
392 source_range: SourceRange,
393) -> Result<Option<FnAttrs>, KclError> {
394 let mut found_attrs = false;
395 let mut fn_attrs = FnAttrs::default();
396 for attr in annotations {
397 if attr.name.is_some() || attr.properties.is_none() {
398 continue;
399 }
400 for p in attr.properties.as_ref().unwrap() {
401 if &*p.key.name == IMPL
402 && let Some(s) = p.value.ident_name()
403 {
404 found_attrs = true;
405 fn_attrs.impl_ = Impl::from_str(s).map_err(|_| {
406 KclError::new_semantic(KclErrorDetails::new(
407 format!(
408 "Invalid value for {} attribute, expected one of: {}",
409 IMPL,
410 IMPL_VALUES.join(", ")
411 ),
412 vec![source_range],
413 ))
414 })?;
415 continue;
416 }
417
418 if &*p.key.name == DEPRECATED
419 && let Some(b) = p.value.literal_bool()
420 {
421 found_attrs = true;
422 fn_attrs.deprecated = b;
423 continue;
424 }
425
426 if &*p.key.name == DEPRECATED_SINCE {
427 let Some(s) = p.value.literal_str() else {
428 return Err(KclError::new_semantic(KclErrorDetails::new(
429 format!("Expected a version string for {DEPRECATED_SINCE}, e.g., \"2.0\""),
430 vec![source_range],
431 )));
432 };
433 let Some(constraint) = VersionConstraint::parse(s) else {
434 return Err(KclError::new_semantic(KclErrorDetails::new(
435 format!(
436 "Invalid version string for {DEPRECATED_SINCE}: `{s}`; expected a dotted integer version, e.g., \"2.0\""
437 ),
438 vec![source_range],
439 )));
440 };
441 found_attrs = true;
442 fn_attrs.deprecated_since = Some(constraint);
443 continue;
444 }
445
446 if &*p.key.name == DOC_CATEGORY {
448 continue;
449 }
450
451 if &*p.key.name == EXPERIMENTAL
452 && let Some(b) = p.value.literal_bool()
453 {
454 found_attrs = true;
455 fn_attrs.experimental = b;
456 continue;
457 }
458
459 if &*p.key.name == INCLUDE_IN_FEATURE_TREE
460 && let Some(b) = p.value.literal_bool()
461 {
462 found_attrs = true;
463 fn_attrs.include_in_feature_tree = b;
464 continue;
465 }
466
467 return Err(KclError::new_semantic(KclErrorDetails::new(
468 format!(
469 "Invalid attribute, expected one of: {IMPL}, {DEPRECATED}, {DEPRECATED_SINCE}, {DOC_CATEGORY}, {EXPERIMENTAL}, {INCLUDE_IN_FEATURE_TREE}, found `{}`",
470 &*p.key.name,
471 ),
472 vec![source_range],
473 )));
474 }
475 }
476
477 Ok(if found_attrs { Some(fn_attrs) } else { None })
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 fn vc(s: &str) -> VersionConstraint {
485 VersionConstraint::parse(s).unwrap()
486 }
487
488 #[test]
489 fn version_constraint_parse_handles_typical_inputs() {
490 assert_eq!(VersionConstraint::parse("1.0"), Some(VersionConstraint(vec![1, 0])));
491 assert_eq!(VersionConstraint::parse("2"), Some(VersionConstraint(vec![2])));
492 assert_eq!(
493 VersionConstraint::parse("2.1.3"),
494 Some(VersionConstraint(vec![2, 1, 3]))
495 );
496 assert_eq!(VersionConstraint::parse(""), None);
497 assert_eq!(VersionConstraint::parse("1.x"), None);
498 assert_eq!(VersionConstraint::parse("1.-1"), None);
499 }
500
501 #[test]
502 fn version_constraint_is_before_compares_numerically() {
503 assert!(vc("2.0").is_before(&vc("3.0")));
504 assert!(vc("2.9").is_before(&vc("2.10")));
505 assert!(vc("9.0").is_before(&vc("10.0")));
506 assert!(vc("3.0").is_before(&vc("3.0.1")));
507 assert!(!vc("3.0").is_before(&vc("3.0")));
508 assert!(!vc("3.0").is_before(&vc("2.0")));
509 assert!(!vc("2.10").is_before(&vc("2.9")));
510 }
511
512 #[test]
513 fn version_constraint_display_round_trips() {
514 assert_eq!(vc("1.0").to_string(), "1.0");
515 assert_eq!(vc("2.1.3").to_string(), "2.1.3");
516 assert_eq!(vc("2").to_string(), "2");
517 }
518
519 #[test]
520 fn version_ge_compares_components_numerically() {
521 assert!(version_ge("1.0", &vc("1.0")));
522 assert!(version_ge("2.0", &vc("1.0")));
523 assert!(version_ge("2.0", &vc("2.0")));
524 assert!(version_ge("10.0", &vc("2.0")));
525 assert!(version_ge("2.1", &vc("2.0")));
526 assert!(!version_ge("1.0", &vc("2.0")));
527 assert!(!version_ge("2.0", &vc("2.1")));
528 assert!(!version_ge("1.99", &vc("2.0")));
529 assert!(!version_ge("bogus", &vc("1.0")));
531 }
532
533 #[test]
534 fn version_ge_supports_prerelease_versions() {
535 assert!(version_ge("3.0-preview", &vc("2.0")));
536 assert!(!version_ge("3.0-preview", &vc("4.0")));
537 }
538}