1use std::collections::BTreeMap;
16use std::fmt::Write as _;
17
18use kui_core::schema::{self, Kind};
19
20use crate::PRELUDE;
21
22struct Alias {
27 name: &'static str,
28 doc: &'static str,
29 ty: &'static str,
30 #[cfg_attr(not(test), allow(dead_code))]
31 strings: &'static [&'static str],
32}
33
34const ALIASES: &[Alias] = &[
37 Alias {
38 name: "kui.Color",
39 doc: "A colour: `0xRRGGBBAA`, `\"#hex\"`, or a `\"$token\"` the host declared.",
40 ty: "integer|string",
41 strings: &["\"#336699\"", "\"#336699cc\"", "\"$accent\""],
42 },
43 Alias {
44 name: "kui.Length",
45 doc: "A length in logical px, or a `\"$token\"`.",
46 ty: "number|string",
47 strings: &["\"$gap\""],
48 },
49 Alias {
50 name: "kui.SizeArg",
51 doc: "A size expression's part: px, a spelling (`\"80%\"`, `\"min(…)\"`), `{ pct = n }` or `{ px = n }`.",
52 ty: "number|string|{ pct: number }|{ px: number }",
53 strings: &["\"80%\""],
54 },
55 Alias {
56 name: "kui.Size",
57 doc: "A size expression (backlog F109), resolved against the parent's content box: px, `\"Npx\"`, `\"N%\"`, `\"min(…)\"`, `\"max(…)\"`, `\"clamp(MIN, TARGET, MAX)\"`, nested — or the same as data, which is never parsed: `{ pct = n }`, `{ px = n }`, `{ min = { … } }`, `{ max = { … } }`, `{ clamp = { MIN, TARGET, MAX } }` (a part a table again, or a spelling). The process keeps 65 536 distinct expressions and never lets one go: past that a new one leaves its prop at its default, with a `size-expressions-full` warning — declare one per layout, not one per frame (backlog RG93).",
58 ty: "kui.SizeArg|{ min: kui.SizeArg[] }|{ max: kui.SizeArg[] }|{ clamp: kui.SizeArg[] }",
59 strings: &["\"clamp(400px, 80%, 1000px)\""],
60 },
61 Alias {
62 name: "kui.Sizing",
63 doc: "A sizing: px, `\"fit\"`, `\"grow\"`, `\"N%\"`, `{ grow = n }`, `{ pct = n }` (n percent), a size expression (`kui.Size`), or a `\"$length\"`.",
64 ty: "\"fit\"|\"grow\"|{ grow: number }|kui.Size",
65 strings: &["\"50%\"", "\"$gap\"", "\"clamp(400px, 80%, 1000px)\""],
66 },
67 Alias {
68 name: "kui.Min",
69 doc: "A lower clamp: px, `\"fit\"`, a size expression (`kui.Size`), or a `\"$length\"`.",
70 ty: "\"fit\"|kui.Size",
71 strings: &["\"$gap\"", "\"50%\""],
72 },
73 Alias {
74 name: "kui.Max",
75 doc: "An upper clamp: px, a size expression (`kui.Size`), or a `\"$length\"`.",
76 ty: "kui.Size",
77 strings: &["\"$gap\"", "\"min(720px, 100%)\""],
78 },
79];
80
81const LUA_KEYWORDS: &[&str] = &[
86 "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
87 "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
88];
89
90const UNIFORM_LIST: &[(&str, bool, &str, &str)] = &[
94 (
95 "key",
96 true,
97 "string",
98 "The container's key, which its scroll geometry is read back by.",
99 ),
100 (
101 "row_h",
102 true,
103 "number",
104 "One row's height: the whole stride, spacing included.",
105 ),
106 (
107 "rows",
108 false,
109 "number",
110 "How many rows the list has; none when left out.",
111 ),
112 (
113 "overscan",
114 false,
115 "number",
116 "Rows built past each end of the viewport; two when left out.",
117 ),
118 (
119 "row_props",
120 false,
121 "fun(i: integer): kui.Props",
122 "Each row's own node's props — its click, stripe, hover; its `index` and `height` stay the list's.",
123 ),
124];
125
126const LIST: &[(&str, bool, &str, &str)] = &[
129 (
130 "key",
131 true,
132 "string",
133 "The container's key, which its scroll geometry is read back by.",
134 ),
135 (
136 "heights",
137 true,
138 "kui.RowHeights",
139 "The heights the list slices by, made once with `row_heights` and kept.",
140 ),
141 (
142 "overscan",
143 false,
144 "number",
145 "Rows built past each end of the viewport; two when left out.",
146 ),
147];
148
149const ROW_HEIGHTS: &[(&str, &str, &str)] = &[
152 ("len", "", "integer"),
153 ("set_len", "rows: integer", "nil"),
154 ("clear", "", "nil"),
155 ("set", "i: integer, h: number", "nil"),
156 ("measured", "i: integer", "number?"),
157 ("get", "i: integer", "number"),
158 ("estimate", "", "number"),
159 ("total", "", "number"),
160 ("offset_of", "i: integer", "number"),
161 ("row_at", "y: number", "integer"),
162];
163
164pub fn luals_meta() -> String {
174 let mut out = String::new();
175 out.push_str(
176 "---@meta kui\n\
177 -- kui-lua's view DSL for lua-language-server, generated from\n\
178 -- kui-core's schema by `kui_lua::luals_meta()`. Regenerate it\n\
179 -- rather than editing it.\n\n",
180 );
181 for a in ALIASES {
182 let _ = writeln!(out, "---{}\n---@alias {} {}", a.doc, a.name, a.ty);
183 }
184 out.push('\n');
185
186 out.push_str(
188 "---Every prop a node table may carry; its children at 1, 2, ….\n---@class kui.Props\n",
189 );
190 for (name, ty, doc) in props_fields() {
191 field(&mut out, name, &ty, doc);
192 }
193 out.push_str("---@field [integer] kui.Node|string\n\n");
194 out.push_str("---A node: what a constructor returns and a view gives back.\n---@class kui.Node: kui.Props\n---@field type string\n\n");
195
196 let mut class_of: BTreeMap<&str, String> = BTreeMap::new();
198 for e in schema::ELEMENTS {
199 let class = format!("kui.{}", e.name);
200 if e.lua_own.is_empty() {
201 for ctor in constructors(e.lua) {
202 class_of.insert(ctor, "kui.Props".into());
203 }
204 continue;
205 }
206 let _ = writeln!(out, "---@class {class}: kui.Props");
207 for own in e.lua_own {
208 field(&mut out, own, "any", e.doc);
209 }
210 out.push('\n');
211 for ctor in constructors(e.lua) {
212 class_of.insert(ctor, class.clone());
213 }
214 }
215
216 out.push_str("---What `uniform_list` reads off its options; every other key is the container's.\n---@class kui.UniformList: kui.Props\n");
218 for (name, required, ty, doc) in UNIFORM_LIST {
219 let opt = if *required { "" } else { "?" };
220 let _ = writeln!(out, "---@field {name}{opt} {ty} {doc}");
221 }
222 out.push('\n');
223
224 out.push_str("---What `list` reads off its options; every other key is the container's.\n---@class kui.List: kui.Props\n");
226 for (name, required, ty, doc) in LIST {
227 let opt = if *required { "" } else { "?" };
228 let _ = writeln!(out, "---@field {name}{opt} {ty} {doc}");
229 }
230 out.push_str(
231 "\n---A variable-height list's row heights: measured where known, the mean of those elsewhere (rows are 0-based).\n---@class kui.RowHeights\n",
232 );
233 for (name, params, ret) in ROW_HEIGHTS {
234 let _ = writeln!(
235 out,
236 "---@field {name} fun(self: kui.RowHeights{}{params}): {ret}",
237 if params.is_empty() { "" } else { ", " }
238 );
239 }
240 out.push_str(
241 "\n---`rows` rows, none measured, each at `estimate` logical px (20 when left out) until measured.\n---@param rows integer\n---@param estimate? number\n---@return kui.RowHeights\nfunction row_heights(rows, estimate) end\n\n",
242 );
243
244 out.push_str(
246 "---An event a view hears: its payload's fields beside `kind`.\n---@class kui.Event\n",
247 );
248 let kinds: Vec<String> = schema::EVENTS
249 .iter()
250 .map(|e| format!("\"{}\"", e.kind))
251 .collect();
252 let _ = writeln!(out, "---@field kind {}", kinds.join("|"));
253 out.push_str("---@field node_key? integer\n---@field [string] any\n\n");
254
255 for f in prelude_functions() {
260 for line in f.doc.lines() {
261 let _ = writeln!(out, "---{line}");
262 }
263 let element = class_of.get(f.name.as_str()).cloned();
264 for p in &f.params {
265 let (opt, ty) = match p.as_str() {
266 "opts" if f.name == "uniform_list" => ("", "kui.UniformList".into()),
267 "opts" if f.name == "list" => ("", "kui.List".into()),
268 "measure" => ("", "fun(i: integer, width: number): number".into()),
269 "t" | "opts" => (
270 if tolerates_nil(&f.body, p) { "?" } else { "" },
271 element.as_deref().unwrap_or("kui.Props").to_string(),
272 ),
273 "s" => ("", "string|number|table".into()),
275 "env" => ("", "table".into()),
276 "row" => ("", "fun(i: integer): kui.Node".into()),
277 "key" => ("", "string|integer".into()),
278 "i" => ("", "integer".into()),
279 "row_h" => ("", "number".into()),
280 _ => ("", "any".into()),
281 };
282 let _ = writeln!(out, "---@param {p}{opt} {ty}");
283 }
284 let ret = match f.name.as_str() {
286 "reveal_row" => "boolean",
287 "rows_in_view" => "integer",
288 _ => "kui.Node",
289 };
290 let _ = writeln!(
291 out,
292 "---@return {ret}\nfunction {}({}) end\n",
293 f.name,
294 f.params.join(", ")
295 );
296 }
297 out
298}
299
300fn props_fields() -> Vec<(&'static str, String, &'static str)> {
304 let mut out = Vec::new();
305 for p in schema::PROPS {
306 for name in lua_names(p.snake_name()) {
307 out.push((name, type_of(&p.kind), p.doc));
308 }
309 }
310 for c in schema::CUSTOM {
311 for name in c.lua_names {
312 out.push((*name, composite_type(name), c.doc));
313 }
314 }
315 out
316}
317
318fn lua_names(snake: &'static str) -> Vec<&'static str> {
322 let mut names: Vec<&'static str> = Vec::new();
323 if !LUA_KEYWORDS.contains(&snake) {
324 names.push(snake);
325 }
326 names.extend(
327 schema::LUA_ALIASES
328 .iter()
329 .filter(|(_, real)| *real == snake)
330 .map(|(alias, _)| *alias),
331 );
332 names
333}
334
335fn composite_type(name: &str) -> String {
339 match name {
340 "size" => "kui.Length".into(),
341 "pad" => "kui.Length|{ all?: kui.Length, x?: kui.Length, y?: kui.Length, l?: kui.Length, r?: kui.Length, t?: kui.Length, b?: kui.Length }".into(),
342 "border" => "{ w?: kui.Length, color?: kui.Color }".into(),
343 "float" => {
344 let presets: Vec<String> = kui_core::FLOAT_PRESETS
345 .iter()
346 .map(|p| format!("\"{p}\""))
347 .collect();
348 format!("{}|table", presets.join("|"))
349 }
350 "key" | "tooltip" | "window_title" => "string".into(),
351 "index" | "row_count" => "number".into(),
352 "windows" => "table".into(),
353 "option_as_alt" => {
354 let names: Vec<String> = kui_core::OptionAsAlt::ALL
355 .iter()
356 .map(|v| format!("\"{}\"", v.name()))
357 .collect();
358 names.join("|")
359 }
360 "clip" | "scroll" | "scroll_x" | "scroll_y" | "key_focus" | "always_on_top"
361 | "secure_input" => "boolean".into(),
362 _ => "any".into(),
363 }
364}
365
366fn field(out: &mut String, name: &str, ty: &str, doc: &str) {
368 let doc = doc.split_whitespace().collect::<Vec<_>>().join(" ");
369 let _ = writeln!(out, "---@field {name}? {ty} {doc}");
370}
371
372fn type_of(kind: &Kind) -> String {
374 match kind {
375 Kind::F32 => "kui.Length".into(),
376 Kind::Color => "kui.Color".into(),
377 Kind::Flag => "boolean".into(),
378 Kind::Enum(names) => names
379 .iter()
380 .map(|n| format!("\"{n}\""))
381 .collect::<Vec<_>>()
382 .join("|"),
383 Kind::Sizing => "kui.Sizing".into(),
384 Kind::Min => "kui.Min".into(),
385 Kind::Max => "kui.Max".into(),
386 Kind::Msg | Kind::Tag => "any".into(),
387 Kind::Str => "string".into(),
388 Kind::Family => "\"sans\"|\"serif\"|\"mono\"|string".into(),
390 Kind::Resource => "integer".into(),
391 Kind::Keyframes | Kind::Enter | Kind::Gradient => "table".into(),
392 }
393}
394
395fn constructors(lua: &str) -> Vec<&str> {
398 lua.split('`')
399 .skip(1)
400 .step_by(2)
401 .filter_map(|form| {
402 let end = form.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))?;
403 let rest = form[end..].trim_start();
404 (end > 0 && (rest.starts_with('{') || rest.starts_with('('))).then(|| &form[..end])
405 })
406 .collect()
407}
408
409struct PreludeFn {
411 name: String,
412 params: Vec<String>,
413 doc: String,
415 body: String,
417}
418
419fn tolerates_nil(body: &str, p: &str) -> bool {
422 body.contains(&format!("{p} = {p} or ")) || body.contains(&format!("if {p} then"))
423}
424
425fn prelude_functions() -> Vec<PreludeFn> {
427 let mut out: Vec<PreludeFn> = Vec::new();
428 let mut doc: Vec<&str> = Vec::new();
429 let mut open = false;
430 for line in PRELUDE.lines() {
431 if open {
432 if line == "end" {
433 open = false;
434 } else if let Some(f) = out.last_mut() {
435 f.body.push_str(line);
436 f.body.push('\n');
437 }
438 continue;
439 }
440 if let Some(c) = line.strip_prefix("--") {
441 doc.push(c.strip_prefix(' ').unwrap_or(c));
442 continue;
443 }
444 if let Some(sig) = line.strip_prefix("function ")
445 && let Some((name, rest)) = sig.split_once('(')
446 && let Some((params, _)) = rest.split_once(')')
447 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
448 {
449 let params = params
450 .split(',')
451 .map(|p| p.trim().to_string())
452 .filter(|p| !p.is_empty())
453 .collect();
454 out.push(PreludeFn {
455 name: name.to_string(),
456 params,
457 doc: doc.join("\n"),
458 body: String::new(),
459 });
460 open = true;
461 }
462 doc.clear();
463 }
464 out
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use mlua::{Lua, Table};
471
472 #[test]
475 fn the_meta_covers_the_schema_and_the_prelude() {
476 let meta = luals_meta();
477 assert!(meta.starts_with("---@meta kui\n"));
478 for p in schema::PROPS {
479 let names = lua_names(p.snake_name());
480 assert!(!names.is_empty(), "{} has no Lua spelling", p.snake_name());
481 for name in names {
482 assert!(
483 meta.contains(&format!("---@field {name}? ")),
484 "{name} missing"
485 );
486 }
487 }
488 assert!(meta.contains("---@field direction? "));
490 assert!(!meta.contains("---@field repeat? "));
491 for f in prelude_functions() {
492 assert!(
493 meta.contains(&format!("function {}(", f.name)),
494 "{} missing",
495 f.name
496 );
497 }
498 assert!(meta.contains("---@param t? kui.Props\n---@return kui.Node\nfunction row(t) end"));
499 assert!(meta.contains("---@param t kui.edit\n"), "edit's own props");
500 assert!(meta.contains("---@param opts kui.UniformList\n"));
501 assert!(meta.contains("---@field initial? any"));
502 assert_eq!(constructors("`row { }`, `column { }`"), ["row", "column"]);
503 assert_eq!(constructors("`text(\"s\", {…})`"), ["text"]);
504 assert!(constructors("`size`").is_empty());
505 }
506
507 fn prelude_lua() -> Lua {
509 let lua = Lua::new();
510 lua.load(PRELUDE).exec().unwrap();
511 lua
512 }
513
514 fn parses(lua: &Lua, props: &str) -> Result<(), String> {
518 let t: Table = lua
519 .load(format!("return {{ {props} }}"))
520 .eval()
521 .map_err(|e| format!("does not compile: {e}"))?;
522 thread_local! {
525 static CORE: &'static kui_core::Core = Box::leak(Box::new(kui_core::Core::new()));
526 }
527 let core: &'static kui_core::Core = CORE.with(|c| *c);
528 let mut refs = crate::Refs::new(core.token_lookup());
529 crate::parse_props(&t, false, &mut refs)
530 .map(|_| ())
531 .map_err(|e| e.to_string())
532 }
533
534 const ROOT_ONLY: &[&str] = &[
538 "window_title",
539 "always_on_top",
540 "secure_input",
541 "option_as_alt",
542 "windows",
543 ];
544
545 fn members(ty: &str) -> Vec<&str> {
547 let mut out = Vec::new();
548 let (mut depth, mut start) = (0, 0);
549 for (i, c) in ty.char_indices() {
550 match c {
551 '{' | '(' => depth += 1,
552 '}' | ')' => depth -= 1,
553 '|' if depth == 0 => {
554 out.push(ty[start..i].trim());
555 start = i + 1;
556 }
557 _ => {}
558 }
559 }
560 out.push(ty[start..].trim());
561 out
562 }
563
564 fn alias(name: &str) -> Option<&'static Alias> {
565 ALIASES.iter().find(|a| a.name == name)
566 }
567
568 fn shape(member: &str) -> Vec<(&str, bool, &str)> {
570 let inner = member.trim_start_matches('{').trim_end_matches('}');
571 let mut out = Vec::new();
572 let (mut depth, mut start) = (0, 0);
573 let mut fields = Vec::new();
574 for (i, c) in inner.char_indices() {
575 match c {
576 '{' | '(' => depth += 1,
577 '}' | ')' => depth -= 1,
578 ',' if depth == 0 => {
579 fields.push(&inner[start..i]);
580 start = i + 1;
581 }
582 _ => {}
583 }
584 }
585 fields.push(&inner[start..]);
586 for f in fields {
587 let Some((name, ty)) = f.split_once(':') else {
588 continue;
589 };
590 let name = name.trim();
591 let (name, required) = match name.strip_suffix('?') {
592 Some(n) => (n, false),
593 None => (name, true),
594 };
595 out.push((name, required, ty.trim()));
596 }
597 out
598 }
599
600 fn samples(ty: &str, strings: &[&str]) -> Vec<String> {
604 let mut out = Vec::new();
605 for m in members(ty) {
606 match m {
607 "number" => out.push("12.5".into()),
608 "integer" => out.push("3".into()),
609 "boolean" => out.push("true".into()),
610 "string" if strings.is_empty() => out.push("\"x\"".into()),
611 "string" => out.extend(strings.iter().map(|s| s.to_string())),
612 "any" | "table" => {}
613 m if m.starts_with('"') => out.push(m.into()),
614 m if m.starts_with('{') => {
615 let fields: Vec<String> = shape(m)
616 .into_iter()
617 .map(|(name, _, ty)| {
618 let v = samples(ty, &[]).into_iter().next().unwrap();
619 format!("{name} = {v}")
620 })
621 .collect();
622 out.push(format!("{{ {} }}", fields.join(", ")));
623 }
624 m if m.ends_with("[]") => {
627 let one = samples(&m[..m.len() - 2], &[]).into_iter().next().unwrap();
628 out.push(format!("{{ {one}, {one}, {one} }}"));
629 }
630 m if m.starts_with("kui.") => {
631 let a = alias(m).unwrap_or_else(|| panic!("{m} is no alias"));
632 out.extend(samples(a.ty, a.strings));
633 }
634 m => panic!("no sample for {m}"),
635 }
636 }
637 out
638 }
639
640 #[test]
645 fn every_annotated_type_parses() {
646 let lua = Lua::new();
647 for c in schema::CUSTOM {
648 for name in c.lua_names {
649 assert_ne!(
650 composite_type(name),
651 "any",
652 "{name}: a composite the meta does not type"
653 );
654 }
655 }
656 let mut failed = Vec::new();
657 for (name, ty, _) in props_fields() {
658 if ROOT_ONLY.contains(&name) {
659 continue;
660 }
661 for v in samples(&ty, &[]) {
662 if let Err(e) = parses(&lua, &format!("{name} = {v}")) {
663 failed.push(format!("{name} = {v} ({ty}): {e}"));
664 }
665 }
666 }
667 assert!(failed.is_empty(), "{}", failed.join("\n"));
668 }
669
670 enum Probe {
672 Int,
673 Float,
674 Bool,
675 Str(&'static str),
676 Table(&'static [&'static str]),
677 }
678
679 const PROBES: &[(&str, Probe)] = &[
680 ("3", Probe::Int),
681 ("12.5", Probe::Float),
682 ("false", Probe::Bool),
683 ("\"x\"", Probe::Str("x")),
684 ("\"$gap\"", Probe::Str("$gap")),
685 ("\"#336699\"", Probe::Str("#336699")),
686 ("\"50%\"", Probe::Str("50%")),
687 ("\"fit\"", Probe::Str("fit")),
688 ("\"grow\"", Probe::Str("grow")),
689 ("\"below\"", Probe::Str("below")),
690 ("{}", Probe::Table(&[])),
691 ("{ grow = 2 }", Probe::Table(&["grow"])),
692 ("{ pct = 50 }", Probe::Table(&["pct"])),
693 ("{ percent = 50 }", Probe::Table(&["percent"])),
694 ];
695
696 fn admits(ty: &str, p: &Probe) -> bool {
700 members(ty).into_iter().any(|m| match (m, p) {
701 ("any", _) => true,
702 ("table", Probe::Table(_)) => true,
703 ("number" | "integer", Probe::Int | Probe::Float) => true,
704 ("boolean", Probe::Bool) => true,
705 ("string", Probe::Str(_)) => true,
706 (m, Probe::Str(s)) if m.starts_with('"') => m == format!("\"{s}\""),
707 (m, Probe::Table(keys)) if m.starts_with('{') => shape(m)
708 .iter()
709 .all(|(name, required, _)| !required || keys.contains(name)),
710 (m, p) if m.starts_with("kui.") => alias(m).is_some_and(|a| admits(a.ty, p)),
711 _ => false,
712 })
713 }
714
715 #[test]
723 fn every_parsed_value_is_admitted() {
724 let lua = Lua::new();
725 let mut failed = Vec::new();
726 for (name, ty, _) in props_fields() {
727 if ty == "boolean" || ROOT_ONLY.contains(&name) {
728 continue;
729 }
730 for (src, p) in PROBES {
731 if parses(&lua, &format!("{name} = {src}")).is_ok() && !admits(&ty, p) {
732 failed.push(format!("{name} = {src} parses; {ty} refuses it"));
733 }
734 }
735 }
736 assert!(failed.is_empty(), "{}", failed.join("\n"));
737 }
738
739 #[test]
742 fn the_keywords_are_lua_s() {
743 let lua = Lua::new();
744 for k in LUA_KEYWORDS {
745 assert!(
746 lua.load(format!("return {{ {k} = 1 }}")).exec().is_err(),
747 "{k} compiles as a key"
748 );
749 }
750 assert!(lua.load("return { direction = 1 }").exec().is_ok());
751 }
752
753 #[test]
757 fn a_param_is_optional_where_the_prelude_takes_nil() {
758 let lua = prelude_lua();
759 let meta = luals_meta();
760 let mut checked = 0;
761 for f in prelude_functions() {
762 for p in f.params.iter().filter(|p| *p == "t" || *p == "opts") {
763 let args: Vec<&str> = f
764 .params
765 .iter()
766 .map(|q| match q.as_str() {
767 q if q == p => "nil",
768 "s" => "\"x\"",
769 "env" => "{ scroll_geometry = function() end, viewport_h = 100 }",
770 "row" => "function(i) return text(tostring(i)) end",
771 _ => "nil",
772 })
773 .collect();
774 let src = format!("return {}({})", f.name, args.join(", "));
775 let takes_nil = lua.load(&src).exec().is_ok();
776 let optional = section(&meta, &f.name).contains(&format!("---@param {p}? "));
777 assert_eq!(
778 optional,
779 takes_nil,
780 "{}: `{p}` marked {} but the prelude {} nil",
781 f.name,
782 if optional { "optional" } else { "required" },
783 if takes_nil { "takes" } else { "refuses" }
784 );
785 checked += 1;
786 }
787 }
788 assert!(section(&meta, "row").contains("---@param t? "));
790 assert!(section(&meta, "button").contains("---@param t kui.button"));
791 assert!(checked > 20, "{checked} parameters");
792 assert!(lua.load("return text(3), tooltip(3)").exec().is_ok());
795 assert!(section(&meta, "text").contains("---@param s string|number|table\n"));
796 }
797
798 fn section<'a>(meta: &'a str, name: &str) -> &'a str {
801 let end = meta
802 .find(&format!("\nfunction {name}("))
803 .unwrap_or_else(|| panic!("{name} missing"));
804 let start = meta[..end].rfind("\n\n").map_or(0, |i| i + 2);
805 &meta[start..end]
806 }
807
808 #[test]
812 fn uniform_list_s_class_is_what_it_reads() {
813 let f = prelude_functions()
814 .into_iter()
815 .find(|f| f.name == "uniform_list")
816 .unwrap();
817 let reads: Vec<&str> = f
818 .body
819 .split("opts.")
820 .skip(1)
821 .map(|r| {
822 let end = r
823 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
824 .unwrap_or(r.len());
825 &r[..end]
826 })
827 .collect();
828 let props: Vec<&str> = props_fields().into_iter().map(|(n, _, _)| n).collect();
829 for (name, _, _, _) in UNIFORM_LIST {
830 assert!(reads.contains(name), "{name} is never read");
831 }
832 for r in &reads {
833 assert!(
834 props.contains(r) || UNIFORM_LIST.iter().any(|(n, ..)| n == r),
835 "opts.{r} is read and typed nowhere"
836 );
837 }
838 let lua = prelude_lua();
839 let env = "{ scroll_geometry = function() end, viewport_h = 100 }";
840 for (name, required, _, _) in UNIFORM_LIST {
841 let opts: Vec<String> = UNIFORM_LIST
842 .iter()
843 .filter(|(n, ..)| n != name)
844 .map(|(n, _, ty, _)| {
845 let v = if *ty == "string" {
846 "\"log\""
847 } else if ty.starts_with("fun") {
848 "function() return {} end"
849 } else {
850 "4"
851 };
852 format!("{n} = {v}")
853 })
854 .collect();
855 let src = format!(
856 "return uniform_list({env}, {{ {} }}, function(i) return text(i) end)",
857 opts.join(", ")
858 );
859 assert_eq!(
860 lua.load(&src).exec().is_err(),
861 *required,
862 "uniform_list without {name}"
863 );
864 }
865 for pad in ["8", "{ t = 8 }", "{ y = 8 }", "{ all = 8 }", "\"$gap\""] {
869 let src = format!(
870 "return uniform_list({env}, {{ key = \"log\", row_h = 4, rows = 9, pad = {pad} }}, \
871 function(i) return text(i) end)"
872 );
873 assert!(lua.load(&src).exec().is_ok(), "pad = {pad}");
874 }
875 }
876
877 #[test]
879 fn the_checks_catch_the_first_cut_s_types() {
880 let lua = Lua::new();
881 assert!(parses(&lua, "width = { percent = 50 }").is_err());
883 assert!(parses(&lua, "key = 3").is_err());
884 assert!(parses(&lua, "index = \"x\"").is_err());
885 assert!(parses(&lua, "size = \"$gap\"").is_ok());
887 assert!(!admits("number", &Probe::Str("$gap")));
888 assert!(parses(&lua, "repeat = \"alternate\"").is_err());
890 assert!(parses(&lua, "direction = \"alternate\"").is_ok());
891 }
892}