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