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