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