1pub use mlua;
12
13use anyhow::{Context, Result, anyhow, bail};
14use mlua::chunk::ChunkMode;
15use mlua::{Function, Lua, Table, Value, Variadic};
16use std::path::{Path, PathBuf};
17
18pub mod bundle;
19pub mod config;
20#[cfg(feature = "dts")]
21pub mod dts;
22#[cfg(feature = "pkg")]
23pub mod pkg;
24pub mod teal;
25pub mod testing;
26
27pub(crate) const PRELUDE_REGISTRY_KEY: &str = "htl.prelude";
30
31const TL_SRC: &str = include_str!("../vendor/tl.lua");
32const LINT_SRC: &str = include_str!("lint.lua");
33const FMT_SRC: &str = include_str!("fmt.lua");
34const PRELUDE: &str = include_str!("prelude.lua");
35
36pub const TEAL_VERSION: &str = "0.24.8";
38
39#[derive(Debug, Clone, Default)]
41pub struct CheckInfo {
42 pub errors: Vec<String>,
44 pub warnings: Vec<String>,
46 pub deps: Vec<PathBuf>,
48 pub lints: Vec<String>,
51 pub requires: Vec<RequireSite>,
54}
55
56#[derive(Debug, Clone)]
58pub struct RequireSite {
59 pub module: String,
60 pub path: Option<PathBuf>,
62 pub line: usize,
63 pub col: usize,
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct ContractResult {
69 pub errors: Vec<String>,
71 pub missing: Option<Vec<String>>,
74 pub missing_at: (usize, usize),
75}
76
77impl Htl {
78 pub fn apply_config(&self, root: &Path, cfg: &config::HtlConfig) -> Result<()> {
81 for p in cfg.search_paths(root) {
82 self.add_path(&p)?;
83 }
84 Ok(())
85 }
86
87 pub fn contract_check(&self, file: &Path, modname: &str, type_path: &str, require_fields: bool) -> Result<ContractResult> {
90 let f: Function = self.h.get("contract_check")?;
91 let t: Table = f.call((path_str(file), modname, type_path, require_fields))?;
92 let errors: Table = t.get("errors")?;
93 let errors = errors.sequence_values::<String>().collect::<mlua::Result<_>>()?;
94 let missing = match t.get::<Option<Table>>("missing")? {
95 Some(m) => Some(m.sequence_values::<String>().collect::<mlua::Result<Vec<_>>>()?),
96 None => None,
97 };
98 let missing_at = (
99 t.get::<Option<usize>>("missing_y")?.unwrap_or(1),
100 t.get::<Option<usize>>("missing_x")?.unwrap_or(1),
101 );
102 Ok(ContractResult { errors, missing, missing_at })
103 }
104}
105
106pub fn contract_lints(h: &Htl, root: &Path, cfg: &config::HtlConfig, file: &Path) -> Result<Vec<String>> {
110 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
111 let file_abs = canon(file);
112 let mut out = Vec::new();
113 if !is_tl_source(&file_abs) {
114 return Ok(out);
115 }
116 let modname = file_abs.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_string();
117 for c in &cfg.contract {
118 let Some(dir) = c.dirs(root).into_iter().map(|d| canon(&d)).find(|d| file_abs.parent() == Some(d.as_path()))
119 else {
120 continue;
121 };
122 if !c.applies_to(&modname) {
123 continue;
124 }
125 h.add_path(&dir)?;
128 h.apply_config(root, cfg)?;
129 let r = h.contract_check(&file_abs, &modname, &c.type_path, c.require_fields)?;
130 for e in &r.errors {
131 let msg = e.splitn(4, ':').last().unwrap_or(e).trim();
133 out.push(format!(
134 "{}:1:1: does not satisfy contract {} ({}): {msg} [htl contract]",
135 file.display(),
136 c.type_path,
137 c.dir
138 ));
139 }
140 if let Some(missing) = &r.missing
141 && !missing.is_empty()
142 {
143 out.push(format!(
144 "{}:{}:{}: returned table lacks declared field(s) of {}: {} [htl contract]",
145 file.display(),
146 r.missing_at.0,
147 r.missing_at.1,
148 c.type_path,
149 missing.join(", ")
150 ));
151 }
152 }
153 Ok(out)
154}
155
156pub fn contract_enforcement_lints(cfg: &config::HtlConfig, cfg_path: &Path, cargo_root: Option<&Path>) -> Vec<String> {
162 let mut out = Vec::new();
163 if cfg.contract.is_empty() {
164 return out;
165 }
166 let Some(root) = cargo_root else { return out };
167 let mut sources = String::new();
168 for sub in ["src", "examples", "tests", "benches"] {
169 let dir = root.join(sub);
170 if !dir.is_dir() {
171 continue;
172 }
173 for e in walkdir::WalkDir::new(&dir).into_iter().flatten() {
174 let p = e.path();
175 if p.is_file()
176 && p.extension().and_then(|s| s.to_str()) == Some("rs")
177 && let Ok(t) = std::fs::read_to_string(p)
178 {
179 sources.push_str(&t);
180 sources.push('\n');
181 }
182 }
183 }
184 let by_config = ["contract_resolvers(", "for_contract(", "for_contract_dir("]
186 .iter()
187 .any(|api| sources.contains(api));
188 for c in &cfg.contract {
189 if c.dirs(root_of(cfg_path)).is_empty() {
192 continue;
193 }
194 let by_hand = sources.contains(&format!("expect_type(\"{}\")", c.type_path));
195 let want_fields = if c.require_fields { ".require_fields()" } else { "" };
196 if !(by_config || by_hand) {
197 let by_hand_hint = if c.dir.contains('*') {
198 String::new() } else {
200 format!("add TealResolver::new(\"{}\").expect_type(\"{}\"){} in the Rust host, or ", c.dir, c.type_path, want_fields)
201 };
202 out.push(format!(
203 "{}:1:1: contract `{}` -> {} is declared but the host does not enforce it: {}build resolvers with \
204 htl::pkg::contract_resolvers(root, &config) [htl contract-unenforced]",
205 cfg_path.display(),
206 c.dir,
207 c.type_path,
208 by_hand_hint
209 ));
210 } else if c.require_fields && !by_config && !sources.contains("require_fields()") {
211 out.push(format!(
212 "{}:1:1: contract `{}` -> {} has require_fields = true but the host never calls .require_fields(): \
213 missing fields will pass at run time [htl contract-unenforced]",
214 cfg_path.display(),
215 c.dir,
216 c.type_path
217 ));
218 }
219 }
220 out
221}
222
223fn root_of(cfg_path: &Path) -> &Path {
224 cfg_path.parent().unwrap_or(Path::new("."))
225}
226
227pub fn require_cycles(infos: &[(PathBuf, CheckInfo)]) -> Vec<String> {
232 use std::collections::{HashMap, HashSet};
233 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
234 let mut edges: HashMap<PathBuf, Vec<(PathBuf, &RequireSite)>> = HashMap::new();
235 let mut display: HashMap<PathBuf, PathBuf> = HashMap::new();
236 for (file, ci) in infos {
237 let from = canon(file);
238 display.insert(from.clone(), file.clone());
239 let list = edges.entry(from).or_default();
240 for r in &ci.requires {
241 if let Some(p) = &r.path {
242 list.push((canon(p), r));
243 }
244 }
245 }
246 let nodes: Vec<PathBuf> = {
247 let mut v: Vec<PathBuf> = edges.keys().cloned().collect();
248 v.sort();
249 v
250 };
251 let mut out = Vec::new();
252 let mut reported: HashSet<Vec<PathBuf>> = HashSet::new();
253 let mut state: HashMap<PathBuf, u8> = HashMap::new(); let mut stack: Vec<(PathBuf, Option<&RequireSite>)> = Vec::new();
255
256 fn dfs<'a>(
257 node: PathBuf,
258 edges: &HashMap<PathBuf, Vec<(PathBuf, &'a RequireSite)>>,
259 state: &mut HashMap<PathBuf, u8>,
260 stack: &mut Vec<(PathBuf, Option<&'a RequireSite>)>,
261 reported: &mut HashSet<Vec<PathBuf>>,
262 display: &HashMap<PathBuf, PathBuf>,
263 out: &mut Vec<String>,
264 ) {
265 state.insert(node.clone(), 1);
266 if let Some(list) = edges.get(&node) {
267 for (to, site) in list {
268 match state.get(to).copied() {
269 Some(1) => {
270 let start = stack.iter().position(|(n, _)| n == to).unwrap_or(0);
272 let mut members: Vec<PathBuf> =
273 stack[start..].iter().map(|(n, _)| n.clone()).chain(std::iter::once(node.clone())).collect();
274 members.dedup();
275 let mut key = members.clone();
276 key.sort();
277 if reported.insert(key) {
278 let name = |p: &PathBuf| {
279 display
280 .get(p)
281 .unwrap_or(p)
282 .file_name()
283 .map(|s| s.to_string_lossy().into_owned())
284 .unwrap_or_else(|| p.display().to_string())
285 };
286 let chain: Vec<String> = members.iter().map(name).chain(std::iter::once(name(to))).collect();
287 let first_file = display.get(&members[0]).cloned().unwrap_or_else(|| members[0].clone());
288 let anchor = stack.get(start + 1).and_then(|(_, s)| *s).unwrap_or(site);
290 out.push(format!(
291 "{}:{}:{}: require cycle: {} (Teal types the back edge as an opaque circular require; \
292 break it by moving shared types into a module both sides require) [htl require-cycle]",
293 first_file.display(),
294 anchor.line,
295 anchor.col,
296 chain.join(" -> ")
297 ));
298 }
299 }
300 Some(2) => {}
301 _ => {
302 stack.push((to.clone(), Some(site)));
303 dfs(to.clone(), edges, state, stack, reported, display, out);
304 stack.pop();
305 }
306 }
307 }
308 }
309 state.insert(node, 2);
310 }
311
312 for n in nodes {
313 if !state.contains_key(&n) {
314 stack.push((n.clone(), None));
315 dfs(n, &edges, &mut state, &mut stack, &mut reported, &display, &mut out);
316 stack.pop();
317 }
318 }
319 out.sort();
320 out
321}
322
323impl CheckInfo {
324 pub fn ok(&self) -> bool {
325 self.errors.is_empty()
326 }
327
328 pub fn clean(&self) -> bool {
330 self.errors.is_empty() && self.warnings.is_empty() && self.lints.is_empty()
331 }
332}
333
334pub struct Htl {
336 lua: Lua,
338 h: Table,
341 split: bool,
343}
344
345pub(crate) struct CheckerHandle(pub(crate) Table);
348
349const RUNTIME_REGISTRY_KEY: &str = "htl.runtime";
350
351const RUNTIME_PRELUDE: &str = r#"
355local R = {}
356
357function R.type_only_module(module_name, decl_path)
358 return setmetatable({}, {
359 __index = function(_, key)
360 error(string.format(
361 "module '%s' is declaration-only here (%s): '%s' has no implementation on this path. " ..
362 "It must be provided by the host program (e.g. a Rust #[host_module] via cargo run) " ..
363 "or by a .tl/.lua module with that name.",
364 module_name, decl_path, tostring(key)), 2)
365 end,
366 })
367end
368
369-- gen(name) -> kind, a, b (see resolve_for_require in the checker prelude)
370function R.install_searcher(gen)
371 table.insert(package.searchers, 2, function(module_name)
372 local kind, a, b = gen(module_name)
373 if kind == "code" then
374 local chunk, lerr = load(a, "@" .. b, "t")
375 if not chunk then
376 error("htl: generated Lua failed to load: " .. tostring(lerr), 0)
377 end
378 return function(modname) return chunk(modname, b) end, b
379 elseif kind == "type_only" then
380 return function() return R.type_only_module(module_name, a) end, a
381 end
382 return a
383 end)
384end
385
386function R.add_path(dir)
387 local templates = dir .. "/?.lua;" .. dir .. "/?/init.lua;" .. dir .. "/?/?.lua"
388 if package.path == nil or package.path == "" then
389 package.path = templates
390 else
391 package.path = templates .. ";" .. package.path
392 end
393end
394
395function R.reset_path()
396 package.path = ""
397end
398
399return R
400"#;
401
402impl Htl {
403 pub fn new() -> Result<Self> {
405 let lua = unsafe { Lua::unsafe_new() };
407 Self::from_lua(lua)
408 }
409
410 pub fn with_checker(checker: &Htl) -> Result<Self> {
417 let lua = unsafe { Lua::unsafe_new() };
419 let r: Table = lua
420 .load(RUNTIME_PRELUDE)
421 .set_name("=htl-runtime")
422 .eval()
423 .context("loading htl runtime prelude")?;
424 lua.set_named_registry_value(RUNTIME_REGISTRY_KEY, r)?;
425 lua.set_app_data(CheckerHandle(checker.h.clone()));
426 let begin: Function = checker.h.get("begin_program")?;
427 begin.call::<()>(())?;
428 Ok(Self { lua, h: checker.h.clone(), split: true })
429 }
430
431 fn runtime(&self) -> Result<Table> {
432 Ok(self.lua.named_registry_value::<Table>(RUNTIME_REGISTRY_KEY)?)
433 }
434
435 pub fn search_path(&self) -> Result<String> {
437 let f: Function = self.h.get("get_path")?;
438 Ok(f.call(())?)
439 }
440
441 pub fn set_search_path(&self, path: &str) -> Result<()> {
443 let f: Function = self.h.get("set_path")?;
444 f.call::<()>(path)?;
445 Ok(())
446 }
447
448 pub fn from_lua(lua: Lua) -> Result<Self> {
450 let tl_loader: Function = lua
451 .load(TL_SRC)
452 .set_name("=tl.lua")
453 .into_function()
454 .context("compiling vendored tl.lua")?;
455 let lint_loader: Function = lua
456 .load(LINT_SRC)
457 .set_name("=htl-lint")
458 .into_function()
459 .context("compiling htl lint.lua")?;
460 let package: Table = lua.globals().get("package")?;
461 let preload: Table = package.get("preload")?;
462 let fmt_loader: Function = lua
463 .load(FMT_SRC)
464 .set_name("=htl-fmt")
465 .into_function()
466 .context("compiling htl fmt.lua")?;
467 preload.set("tl", tl_loader)?;
468 preload.set("htl.lint", lint_loader)?;
469 preload.set("htl.fmt", fmt_loader)?;
470 let h: Table = lua
471 .load(PRELUDE)
472 .set_name("=htl-prelude")
473 .eval()
474 .context("loading htl prelude")?;
475 lua.set_named_registry_value(PRELUDE_REGISTRY_KEY, h.clone())?;
476 Ok(Self { lua, h, split: false })
477 }
478
479 pub fn lua(&self) -> &Lua {
480 &self.lua
481 }
482
483 pub fn check(&self, file: &Path) -> Result<CheckInfo> {
485 let f: Function = self.h.get("check")?;
486 let t: Table = f.call(path_str(file))?;
487 read_checkinfo(&t)
488 }
489
490 pub fn gen_lua(&self, file: &Path) -> Result<(Option<String>, CheckInfo)> {
492 let f: Function = self.h.get("gen")?;
493 let (code, t): (Option<String>, Table) = f.call(path_str(file))?;
494 Ok((code, read_checkinfo(&t)?))
495 }
496
497 pub fn configure_lints(&self, spec: &str) -> Result<()> {
499 let f: Function = self.h.get("set_lints")?;
500 let (ok, err): (Option<bool>, Option<String>) = f.call(spec)?;
501 if ok.unwrap_or(false) {
502 Ok(())
503 } else {
504 bail!("{}", err.unwrap_or_else(|| "invalid lint spec".into()))
505 }
506 }
507
508 pub fn lint_rules(&self) -> Result<Vec<String>> {
510 let f: Function = self.h.get("lint_rules")?;
511 let t: Table = f.call(())?;
512 Ok(t.sequence_values::<String>().collect::<mlua::Result<_>>()?)
513 }
514
515 pub fn format_file(&self, file: &Path, indent: usize) -> Result<String> {
517 let f: Function = self.h.get("format")?;
518 let (out, err): (Option<String>, Option<String>) = f.call((path_str(file), indent))?;
519 out.ok_or_else(|| anyhow!("{}", err.unwrap_or_else(|| "format failed".into())))
520 }
521
522 pub fn reset_search_path(&self) -> Result<()> {
525 let f: Function = self.h.get("reset_path")?;
526 f.call::<()>(())?;
527 if self.split {
528 let f: Function = self.runtime()?.get("reset_path")?;
529 f.call::<()>(())?;
530 }
531 Ok(())
532 }
533
534 pub fn add_layout_paths(&self, file: &Path) -> Result<()> {
538 let dir = parent_dir(file);
539 self.add_path(&dir)?;
540 if dir.file_name().is_some_and(|n| n == "tests")
541 && let Some(root) = dir.parent()
542 {
543 self.add_path(root)?;
544 let src = root.join("src");
545 if src.is_dir() {
546 self.add_path(&src)?;
547 }
548 }
549 Ok(())
550 }
551
552 pub fn add_path(&self, dir: &Path) -> Result<()> {
554 let f: Function = self.h.get("add_path")?;
555 f.call::<()>(path_str(dir))?;
556 if self.split {
557 let f: Function = self.runtime()?.get("add_path")?;
559 f.call::<()>(path_str(dir))?;
560 }
561 Ok(())
562 }
563
564 pub fn install_searcher(&self) -> Result<()> {
566 if self.split {
567 let gen_fn: Function = self.h.get("gen_for_require")?;
569 let bridge = self.lua.create_function(move |_, name: String| {
570 let (kind, a, b): (String, Option<String>, Option<String>) = gen_fn.call(name)?;
571 Ok((kind, a, b))
572 })?;
573 let f: Function = self.runtime()?.get("install_searcher")?;
574 f.call::<()>(bridge)?;
575 return Ok(());
576 }
577 let f: Function = self.h.get("install_searcher")?;
578 f.call::<()>(())?;
579 Ok(())
580 }
581
582 pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
584 let loader = self
585 .lua
586 .load(lua_src)
587 .set_name(format!("={name}"))
588 .into_function()
589 .with_context(|| format!("compiling preloaded module {name}"))?;
590 self.preload_table()?.set(name, loader)?;
591 Ok(())
592 }
593
594 pub fn preload_bytes(&self, name: &str, bytecode: &[u8]) -> Result<()> {
596 let loader = self
597 .lua
598 .load(bytecode)
599 .set_name(format!("={name}"))
600 .set_mode(ChunkMode::Binary)
601 .into_function()
602 .with_context(|| format!("loading bytecode for module {name}"))?;
603 self.preload_table()?.set(name, loader)?;
604 Ok(())
605 }
606
607 pub fn exec_bytes(&self, bytecode: &[u8], chunk_name: &str, args: &[String]) -> Result<()> {
609 let f = self
610 .lua
611 .load(bytecode)
612 .set_name(chunk_name)
613 .set_mode(ChunkMode::Binary)
614 .into_function()?;
615 let va: Variadic<String> = args.iter().cloned().collect();
616 f.call::<()>(va)?;
617 Ok(())
618 }
619
620 pub fn preload_value(&self, name: &str, value: impl mlua::IntoLua) -> Result<()> {
622 let value = value.into_lua(&self.lua)?;
623 let loader = self
624 .lua
625 .create_function(move |_, ()| Ok(value.clone()))?;
626 self.preload_table()?.set(name, loader)?;
627 Ok(())
628 }
629
630 fn preload_table(&self) -> Result<Table> {
631 let package: Table = self.lua.globals().get("package")?;
632 Ok(package.get("preload")?)
633 }
634
635 pub fn set_arg(&self, script: &str, args: &[String]) -> Result<()> {
637 let t = self.lua.create_table()?;
638 t.set(0, script)?;
639 for (i, a) in args.iter().enumerate() {
640 t.set(i + 1, a.as_str())?;
641 }
642 self.lua.globals().set("arg", t)?;
643 Ok(())
644 }
645
646 pub fn exec(&self, lua_src: &str, chunk_name: &str, args: &[String]) -> Result<()> {
648 let f = self
649 .lua
650 .load(lua_src)
651 .set_name(chunk_name)
652 .into_function()?;
653 let va: Variadic<String> = args.iter().cloned().collect();
654 f.call::<()>(va)?;
655 Ok(())
656 }
657
658 pub fn run_file(&self, file: &Path, args: &[String]) -> Result<CheckInfo> {
661 self.add_path(&parent_dir(file))?;
662 self.install_searcher()?;
663 self.set_arg(&file.to_string_lossy(), args)?;
664 let (code, ci) = self.gen_lua(file)?;
665 let Some(code) = code else { return Ok(ci) };
666 self.exec(&code, &format!("@{}", file.display()), args)?;
667 Ok(ci)
668 }
669
670 pub fn compile(&self, name: &str, lua_src: &str) -> Result<Vec<u8>> {
672 let f = self
673 .lua
674 .load(lua_src)
675 .set_name(format!("={name}"))
676 .into_function()
677 .with_context(|| format!("compiling generated Lua for {name}"))?;
678 Ok(f.dump(true))
679 }
680
681 pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
683 let modules = b.modules.clone();
684 let searcher = self.lua.create_function(move |lua, name: String| {
685 match modules.iter().find(|(n, _)| *n == name) {
686 Some((_, bc)) => {
687 let f = lua
688 .load(bc.as_slice())
689 .set_name(format!("={name}"))
690 .set_mode(ChunkMode::Binary)
691 .into_function()?;
692 Ok(Value::Function(f))
693 }
694 None => Ok(Value::String(
695 lua.create_string(format!("\n\tno bundled module '{name}'"))?,
696 )),
697 }
698 })?;
699 let package: Table = self.lua.globals().get("package")?;
700 let searchers: Table = package.get("searchers")?;
701 searchers.raw_insert(2, searcher)?;
702 Ok(())
703 }
704
705 pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
707 let entry_bc = b
708 .modules
709 .iter()
710 .find(|(n, _)| *n == b.entry)
711 .map(|(_, bc)| bc.clone())
712 .ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
713 self.install_bundle(b)?;
714 self.set_arg(&b.entry, args)?;
715 let main: Function = self
716 .lua
717 .load(entry_bc.as_slice())
718 .set_name(format!("={}", b.entry))
719 .set_mode(ChunkMode::Binary)
720 .into_function()?;
721 let va: Variadic<String> = args.iter().cloned().collect();
722 main.call::<()>(va)?;
723 Ok(())
724 }
725}
726
727fn path_str(p: &Path) -> String {
728 p.to_string_lossy().into_owned()
729}
730
731pub fn user_message(err: &anyhow::Error) -> String {
740 fn from_mlua(e: &mlua::Error) -> String {
741 match e {
742 mlua::Error::CallbackError { cause, .. } => from_mlua(cause),
743 mlua::Error::ExternalError(ext) => ext.to_string(),
744 mlua::Error::WithContext { cause, .. } => from_mlua(cause),
745 other => strip_traceback(&other.to_string()),
746 }
747 }
748 if let Some(e) = err.downcast_ref::<mlua::Error>() {
749 return from_mlua(e);
750 }
751 strip_traceback(&format!("{err:#}"))
752}
753
754pub fn strip_traceback(text: &str) -> String {
756 let cut = text.find("\nstack traceback:").unwrap_or(text.len());
757 text[..cut].trim_end().to_string()
758}
759
760pub fn write_if_changed(path: &Path, text: &str) -> std::io::Result<bool> {
763 if let Ok(cur) = std::fs::read_to_string(path)
764 && cur == text
765 {
766 return Ok(false);
767 }
768 if let Some(dir) = path.parent() {
769 std::fs::create_dir_all(dir)?;
770 }
771 std::fs::write(path, text)?;
772 Ok(true)
773}
774
775pub fn parent_dir(file: &Path) -> PathBuf {
777 let dir = file.parent().unwrap_or(Path::new("."));
778 if dir.as_os_str().is_empty() { PathBuf::from(".") } else { dir.to_path_buf() }
779}
780
781fn read_checkinfo(t: &Table) -> Result<CheckInfo> {
782 let seq = |key: &str| -> Result<Vec<String>> {
783 let inner: Table = t.get(key)?;
784 Ok(inner.sequence_values::<String>().collect::<mlua::Result<_>>()?)
785 };
786 let mut requires = Vec::new();
787 if let Ok(list) = t.get::<Table>("requires") {
788 for r in list.sequence_values::<Table>() {
789 let r = r?;
790 requires.push(RequireSite {
791 module: r.get::<String>("name")?,
792 path: r.get::<Option<String>>("path")?.map(PathBuf::from),
793 line: r.get::<Option<usize>>("y")?.unwrap_or(0),
794 col: r.get::<Option<usize>>("x")?.unwrap_or(0),
795 });
796 }
797 }
798 Ok(CheckInfo {
799 errors: seq("errors")?,
800 warnings: seq("warnings")?,
801 deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
802 lints: seq("lints")?,
803 requires,
804 })
805}
806
807pub fn is_tl_source(p: &Path) -> bool {
809 let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
810 p.is_file() && name.ends_with(".tl") && !name.ends_with(".d.tl")
811}
812
813pub const SKIP_DIRS: &[&str] = &["target", "node_modules", ".mlua-pkgs", ".git"];
816
817pub fn is_skipped_dir(path: &Path, extra: &[PathBuf]) -> bool {
821 if !path.is_dir() {
822 return false;
823 }
824 let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
825 if SKIP_DIRS.contains(&name) || (name.starts_with('.') && name.len() > 1) {
826 return true;
827 }
828 extra.iter().any(|e| same_dir(path, e))
829}
830
831fn same_dir(a: &Path, b: &Path) -> bool {
832 match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
833 (Ok(x), Ok(y)) => x == y,
834 _ => a == b,
835 }
836}
837
838#[cfg(feature = "pkg")]
842pub fn project_skip_dirs(root: &Path) -> Vec<PathBuf> {
843 match pkg::Project::find(root) {
844 Some(p) => vec![p.pkgs_dir],
845 None => Vec::new(),
846 }
847}
848
849#[cfg(not(feature = "pkg"))]
850pub fn project_skip_dirs(_root: &Path) -> Vec<PathBuf> {
851 Vec::new()
852}
853
854pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
858 let mut out = Vec::new();
859 for p in paths {
860 if p.is_dir() {
861 let extra = project_skip_dirs(p);
862 let root = p.clone();
863 let walker = walkdir::WalkDir::new(p)
864 .sort_by_file_name()
865 .into_iter()
866 .filter_entry(move |e| e.path() == root || !is_skipped_dir(e.path(), &extra));
867 for e in walker {
868 let e = e?;
869 if is_tl_source(e.path()) {
870 out.push(e.path().to_path_buf());
871 }
872 }
873 } else if p.is_file() {
874 out.push(p.clone());
875 } else {
876 bail!("no such file or directory: {}", p.display());
877 }
878 }
879 Ok(out)
880}
881
882pub fn module_name(root: &Path, file: &Path) -> Result<String> {
884 let rel = file.strip_prefix(root)?.with_extension("");
885 let mut parts: Vec<String> = rel
886 .components()
887 .map(|c| c.as_os_str().to_string_lossy().into_owned())
888 .collect();
889 if parts.last().map(|s| s == "init").unwrap_or(false) {
890 parts.pop();
891 }
892 if parts.is_empty() {
893 bail!("cannot derive module name for {}", file.display());
894 }
895 Ok(parts.join("."))
896}