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 cache;
20#[cfg(feature = "dts")]
21pub mod cexport;
22pub mod config;
23pub mod contract;
24#[cfg(feature = "dts")]
25pub mod dep_dts;
26pub mod diagnostic;
27#[cfg(feature = "dts")]
28pub mod dts;
29#[cfg(feature = "ffi")]
30pub mod ffi;
31pub mod fix;
32pub mod link;
35pub mod lint;
36#[cfg(feature = "pkg")]
37pub mod pkg;
38#[cfg(all(feature = "pkg", feature = "dts"))]
43pub mod project;
44#[cfg(all(feature = "pkg", feature = "dts"))]
48pub mod resolve;
49pub mod teal;
50pub mod testing;
51#[cfg(all(feature = "pkg", feature = "dts"))]
54pub mod unused;
55
56pub use diagnostic::{Diagnostic, Severity};
57
58pub(crate) const PRELUDE_REGISTRY_KEY: &str = "htl.prelude";
61
62const TL_SRC: &str = include_str!("../vendor/tl.lua");
63const LINT_SRC: &str = include_str!("lint.lua");
64const FMT_SRC: &str = include_str!("fmt.lua");
65const PRELUDE: &str = include_str!("prelude.lua");
66
67pub fn checker_identity() -> &'static str {
76 static ID: std::sync::OnceLock<String> = std::sync::OnceLock::new();
77 ID.get_or_init(|| {
78 let mut h = blake3::Hasher::new();
79 for src in [TL_SRC, LINT_SRC, FMT_SRC, PRELUDE] {
80 h.update(src.as_bytes());
81 h.update(b"\0");
82 }
83 h.finalize().to_hex().to_string()
84 })
85}
86
87pub const TEAL_VERSION: &str = "0.24.8";
89
90#[derive(Debug, Clone, Default)]
92pub struct CheckInfo {
93 pub errors: Vec<String>,
95 pub warnings: Vec<String>,
97 pub deps: Vec<PathBuf>,
99 pub lints: Vec<String>,
102 pub requires: Vec<RequireSite>,
105 pub error_fixes: Vec<Option<Fix>>,
107 pub lint_fixes: Vec<Option<Fix>>,
109 pub dependency_errors: Vec<DependencyError>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct DependencyError {
127 pub file: PathBuf,
129 pub required_by: PathBuf,
132 pub text: String,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
141#[serde(rename_all = "lowercase")]
142pub enum Applicability {
143 Safe,
145 Unsafe,
147 Suggest,
149}
150
151impl Applicability {
152 pub fn as_str(self) -> &'static str {
153 match self {
154 Applicability::Safe => "safe",
155 Applicability::Unsafe => "unsafe",
156 Applicability::Suggest => "suggest",
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
164pub struct Edit {
165 pub line: usize,
166 pub col: usize,
167 pub end_line: usize,
168 pub end_col: usize,
169 pub text: String,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
177pub struct Fix {
178 pub applicability: Applicability,
179 pub edits: Vec<Edit>,
180}
181
182#[derive(Debug, Clone)]
184pub struct RequireSite {
185 pub module: String,
186 pub path: Option<PathBuf>,
188 pub line: usize,
189 pub col: usize,
190}
191
192#[derive(Debug, Clone)]
200pub struct FunctionSpan {
201 pub name: String,
203 pub line: usize,
205 pub last: usize,
207}
208
209pub type CoverageSpans = (Vec<(usize, usize)>, Vec<FunctionSpan>);
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
217#[serde(rename_all = "lowercase")]
218pub enum ModuleKind {
219 Source,
220 Declaration,
221 Lua,
222}
223
224impl ModuleKind {
225 fn of(s: &str) -> Self {
226 match s {
227 "source" => Self::Source,
228 "declaration" => Self::Declaration,
229 _ => Self::Lua,
230 }
231 }
232
233 pub fn as_str(self) -> &'static str {
235 match self {
236 Self::Source => "source",
237 Self::Declaration => "declaration",
238 Self::Lua => "lua",
239 }
240 }
241}
242
243impl std::fmt::Display for ModuleKind {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.write_str(self.as_str())
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct ModuleCandidate {
252 pub path: PathBuf,
253 pub kind: ModuleKind,
254 pub dir: PathBuf,
256}
257
258#[derive(Debug, Clone, Default)]
260pub struct ContractResult {
261 pub errors: Vec<String>,
263 pub missing: Option<Vec<String>>,
266 pub missing_at: (usize, usize),
267 pub bad_require_fields: Vec<String>,
271}
272
273impl Htl {
274 pub fn apply_config(&self, root: &Path, cfg: &config::HtlConfig) -> Result<()> {
277 self.add_search_paths(&cfg.search_paths(root))
278 }
279
280 pub fn add_search_paths(&self, dirs: &[PathBuf]) -> Result<()> {
289 for p in dirs.iter().rev() {
290 self.add_path(p)?;
291 }
292 Ok(())
293 }
294
295 pub fn contract_check(
298 &self,
299 file: &Path,
300 modname: &str,
301 type_path: &str,
302 require_fields: &config::RequireFields,
303 ) -> Result<ContractResult> {
304 let f: Function = self.h.get("contract_check")?;
305 let wanted = match require_fields.named() {
307 Some(names) => mlua::Value::Table(self.lua().create_sequence_from(names.to_vec())?),
308 None => mlua::Value::Boolean(require_fields.is_on()),
309 };
310 let t: Table = f.call((path_str(file), modname, type_path, wanted))?;
311 let errors: Table = t.get("errors")?;
312 let errors = errors
313 .sequence_values::<String>()
314 .collect::<mlua::Result<_>>()?;
315 let missing = match t.get::<Option<Table>>("missing")? {
316 Some(m) => Some(
317 m.sequence_values::<String>()
318 .collect::<mlua::Result<Vec<_>>>()?,
319 ),
320 None => None,
321 };
322 let missing_at = (
323 t.get::<Option<usize>>("missing_y")?.unwrap_or(1),
324 t.get::<Option<usize>>("missing_x")?.unwrap_or(1),
325 );
326 let bad_require_fields = match t.get::<Option<Table>>("bad_require_fields")? {
327 Some(b) => b
328 .sequence_values::<String>()
329 .collect::<mlua::Result<Vec<_>>>()?,
330 None => Vec::new(),
331 };
332 Ok(ContractResult {
333 errors,
334 missing,
335 missing_at,
336 bad_require_fields,
337 })
338 }
339}
340
341pub fn contract_lints(
348 h: &Htl,
349 root: &Path,
350 cfg: &config::HtlConfig,
351 contracts: &[contract::Resolved],
352 file: &Path,
353) -> Result<Vec<String>> {
354 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
355 let file_abs = canon(file);
356 let mut out = Vec::new();
357 if !is_tl_source(&file_abs) {
358 return Ok(out);
359 }
360 let modname = file_abs
361 .file_stem()
362 .and_then(|s| s.to_str())
363 .unwrap_or("")
364 .to_string();
365 for c in contracts {
366 let Some(dir) = c
367 .dirs(root)
368 .into_iter()
369 .map(|d| canon(&d))
370 .find(|d| file_abs.parent() == Some(d.as_path()))
371 else {
372 continue;
373 };
374 if !c.applies_to(&modname) {
375 continue;
376 }
377 h.add_path(&dir)?;
381 h.apply_config(root, cfg)?;
382 let r = h.contract_check(&file_abs, &modname, &c.type_path, &c.require_fields)?;
383 if !r.bad_require_fields.is_empty() {
384 out.push(format!(
387 "{}:{}:1: ---@required on field(s) {} does not declare: {} [htl contract]",
388 c.declared_in.display(),
389 c.declared_at,
390 c.type_path,
391 r.bad_require_fields.join(", ")
392 ));
393 continue;
394 }
395 for e in &r.errors {
396 let msg = diagnostic::position(e)
399 .map_or(e.as_str(), |(_, _, _, msg)| msg)
400 .trim();
401 out.push(format!(
402 "{}:1:1: does not satisfy contract {} ({}): {msg} [htl contract]",
403 file.display(),
404 c.type_path,
405 c.dir
406 ));
407 }
408 if let Some(missing) = &r.missing
409 && !missing.is_empty()
410 {
411 out.push(format!(
412 "{}:{}:{}: returned table lacks declared field(s) of {}: {} [htl contract]",
413 file.display(),
414 r.missing_at.0,
415 r.missing_at.1,
416 c.type_path,
417 missing.join(", ")
418 ));
419 }
420 }
421 Ok(out)
422}
423
424pub fn declaration_conflict_lints(
449 h: &Htl,
450 file: &Path,
451 info: &CheckInfo,
452 host_modules: &[String],
453) -> Result<Vec<String>> {
454 let f: Function = h.h.get("declaration_sites")?;
455 let mut out = Vec::new();
456 let mut seen: Vec<&str> = Vec::new();
457 for site in &info.requires {
458 let Some(read) = site.path.as_ref() else {
459 continue;
460 };
461 if seen.contains(&site.module.as_str()) {
463 continue;
464 }
465 if !is_declaration(read) {
466 if host_modules.contains(&site.module) {
467 seen.push(&site.module);
468 out.push(format!(
469 "{}:{}:{}: {} is a host module of this crate and also {}: the check \
470 reads the file, the run loads the host — package.preload is consulted \
471 before any path searcher, so what is checked here is not what runs \
472 [htl host-module-shadowed]",
473 file.display(),
474 site.line,
475 site.col,
476 site.module,
477 read.display(),
478 ));
479 }
480 continue;
481 }
482 let sites: Vec<String> = f
483 .call::<Table>(site.module.as_str())?
484 .sequence_values::<String>()
485 .collect::<mlua::Result<_>>()?;
486 let shadowed: Vec<&str> = sites
487 .iter()
488 .map(|s| s.as_str())
489 .filter(|s| !same_file(Path::new(s), read))
490 .collect();
491 if shadowed.is_empty() {
492 continue;
493 }
494 seen.push(&site.module);
495 out.push(format!(
496 "{}:{}:{}: {} is declared more than once on the search path: {} is read, {} {} not [htl duplicate-declaration]",
497 file.display(),
498 site.line,
499 site.col,
500 site.module,
501 read.display(),
502 shadowed.join(" and "),
503 if shadowed.len() == 1 { "is" } else { "are" },
504 ));
505 }
506 Ok(out)
507}
508
509pub fn contract_enforcement_lints(
525 cfg_path: &Path,
526 contracts: &[contract::Resolved],
527 cargo_root: Option<&Path>,
528) -> Vec<String> {
529 let mut out = Vec::new();
530 if contracts.is_empty() {
531 return out;
532 }
533 let Some(root) = cargo_root else { return out };
534 let mut sources = String::new();
535 for sub in ["src", "examples", "tests", "benches"] {
536 let dir = root.join(sub);
537 if !dir.is_dir() {
538 continue;
539 }
540 for e in walkdir::WalkDir::new(&dir).into_iter().flatten() {
541 let p = e.path();
542 if p.is_file()
543 && p.extension().and_then(|s| s.to_str()) == Some("rs")
544 && let Ok(t) = std::fs::read_to_string(p)
545 {
546 sources.push_str(&t);
547 sources.push('\n');
548 }
549 }
550 }
551 let by_config = sources.contains("contract_resolvers(");
552 for c in contracts {
553 if c.dirs(root_of(cfg_path)).is_empty() {
556 continue;
557 }
558 match &c.enforced_by {
559 Some(p) => {
563 let at = config::resolve_path(root_of(cfg_path), p);
564 if !at.exists() {
565 out.push(format!(
566 "{}:1:1: contract {} -> {} says it is enforced by {:?}, and there \
567 is no such file: name where the enforcement lives, or drop the \
568 key and let the scan look for \
569 htl::pkg::contract_resolvers(root, &config) \
570 [htl contract-unenforced]",
571 cfg_path.display(),
572 c.dir,
573 c.type_path,
574 p,
575 ));
576 }
577 }
578 None if !by_config => out.push(format!(
579 "{}:{}:1: contract {} -> {} is declared but the host does not enforce it: \
580 build resolvers with htl::pkg::contract_resolvers(root, &config), or say \
581 where it is enforced with [[contract]] enforced_by \
582 [htl contract-unenforced]",
583 c.declared_in.display(),
584 c.declared_at,
585 c.dir,
586 c.type_path,
587 )),
588 None => {}
589 }
590 }
591 out
592}
593
594fn root_of(cfg_path: &Path) -> &Path {
595 cfg_path.parent().unwrap_or(Path::new("."))
596}
597
598pub fn require_cycles(infos: &[(PathBuf, CheckInfo)]) -> Vec<String> {
603 use std::collections::{HashMap, HashSet};
604 let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
605 let mut edges: HashMap<PathBuf, Vec<(PathBuf, &RequireSite)>> = HashMap::new();
606 let mut display: HashMap<PathBuf, PathBuf> = HashMap::new();
607 for (file, ci) in infos {
608 let from = canon(file);
609 display.insert(from.clone(), file.clone());
610 let list = edges.entry(from).or_default();
611 for r in &ci.requires {
612 if let Some(p) = &r.path {
613 list.push((canon(p), r));
614 }
615 }
616 }
617 let nodes: Vec<PathBuf> = {
618 let mut v: Vec<PathBuf> = edges.keys().cloned().collect();
619 v.sort();
620 v
621 };
622 let mut out = Vec::new();
623 let mut reported: HashSet<Vec<PathBuf>> = HashSet::new();
624 let mut state: HashMap<PathBuf, u8> = HashMap::new(); let mut stack: Vec<(PathBuf, Option<&RequireSite>)> = Vec::new();
626
627 fn dfs<'a>(
628 node: PathBuf,
629 edges: &HashMap<PathBuf, Vec<(PathBuf, &'a RequireSite)>>,
630 state: &mut HashMap<PathBuf, u8>,
631 stack: &mut Vec<(PathBuf, Option<&'a RequireSite>)>,
632 reported: &mut HashSet<Vec<PathBuf>>,
633 display: &HashMap<PathBuf, PathBuf>,
634 out: &mut Vec<String>,
635 ) {
636 state.insert(node.clone(), 1);
637 if let Some(list) = edges.get(&node) {
638 for (to, site) in list {
639 match state.get(to).copied() {
640 Some(1) => {
641 let start = stack.iter().position(|(n, _)| n == to).unwrap_or(0);
643 let mut members: Vec<PathBuf> = stack[start..]
644 .iter()
645 .map(|(n, _)| n.clone())
646 .chain(std::iter::once(node.clone()))
647 .collect();
648 members.dedup();
649 let mut key = members.clone();
650 key.sort();
651 if reported.insert(key) {
652 let name = |p: &PathBuf| {
653 display
654 .get(p)
655 .unwrap_or(p)
656 .file_name()
657 .map(|s| s.to_string_lossy().into_owned())
658 .unwrap_or_else(|| p.display().to_string())
659 };
660 let chain: Vec<String> = members
661 .iter()
662 .map(name)
663 .chain(std::iter::once(name(to)))
664 .collect();
665 let first_file = display
666 .get(&members[0])
667 .cloned()
668 .unwrap_or_else(|| members[0].clone());
669 let anchor = stack.get(start + 1).and_then(|(_, s)| *s).unwrap_or(site);
671 out.push(format!(
672 "{}:{}:{}: require cycle: {} (Teal types the back edge as an opaque circular require; \
673 break it by moving shared types into a module both sides require) [htl require-cycle]",
674 first_file.display(),
675 anchor.line,
676 anchor.col,
677 chain.join(" -> ")
678 ));
679 }
680 }
681 Some(2) => {}
682 _ => {
683 stack.push((to.clone(), Some(site)));
684 dfs(to.clone(), edges, state, stack, reported, display, out);
685 stack.pop();
686 }
687 }
688 }
689 }
690 state.insert(node, 2);
691 }
692
693 for n in nodes {
694 if !state.contains_key(&n) {
695 stack.push((n.clone(), None));
696 dfs(
697 n,
698 &edges,
699 &mut state,
700 &mut stack,
701 &mut reported,
702 &display,
703 &mut out,
704 );
705 stack.pop();
706 }
707 }
708 out.sort();
709 out
710}
711
712impl CheckInfo {
713 pub fn ok(&self) -> bool {
714 self.errors.is_empty()
715 }
716
717 pub fn clean(&self) -> bool {
719 self.errors.is_empty() && self.warnings.is_empty() && self.lints.is_empty()
720 }
721
722 pub fn diagnostics(&self) -> Vec<Diagnostic> {
729 let mut out = self.warning_diagnostics();
730 out.extend(self.lint_diagnostics());
731 out.extend(self.error_diagnostics());
732 out
733 }
734
735 pub fn error_diagnostics(&self) -> Vec<Diagnostic> {
737 parsed(Severity::Error, &self.errors, &self.error_fixes)
738 }
739
740 pub fn warning_diagnostics(&self) -> Vec<Diagnostic> {
742 parsed(Severity::Warning, &self.warnings, &[])
743 }
744
745 pub fn lint_diagnostics(&self) -> Vec<Diagnostic> {
747 parsed(Severity::Lint, &self.lints, &self.lint_fixes)
748 }
749}
750
751fn parsed(severity: Severity, texts: &[String], fixes: &[Option<Fix>]) -> Vec<Diagnostic> {
753 texts
754 .iter()
755 .enumerate()
756 .map(|(i, text)| {
757 let mut d = Diagnostic::parse(severity, text);
758 d.fix = fixes.get(i).and_then(|f| f.clone());
759 d
760 })
761 .collect()
762}
763
764pub struct Htl {
766 lua: Lua,
768 h: Table,
771 split: bool,
773}
774
775pub(crate) struct CheckerHandle(pub(crate) Table);
778
779const RUNTIME_REGISTRY_KEY: &str = "htl.runtime";
780
781const RUNTIME_PRELUDE: &str = r#"
785local R = {}
786
787function R.type_only_module(module_name, decl_path)
788 return setmetatable({}, {
789 __index = function(_, key)
790 error(string.format(
791 "module '%s' is declaration-only here (%s): '%s' has no implementation on this path. " ..
792 "It must be provided by the host program (e.g. a Rust #[host_module] via cargo run) " ..
793 "or by a .tl/.lua module with that name.",
794 module_name, decl_path, tostring(key)), 2)
795 end,
796 })
797end
798
799-- gen(name) -> kind, a, b (see resolve_for_require in the checker prelude)
800function R.install_searcher(gen)
801 table.insert(package.searchers, 2, function(module_name)
802 local kind, a, b = gen(module_name)
803 if kind == "code" then
804 local chunk, lerr = load(a, "@" .. b, "t")
805 if not chunk then
806 error("htl: generated Lua failed to load: " .. tostring(lerr), 0)
807 end
808 return function(modname) return chunk(modname, b) end, b
809 elseif kind == "type_only" then
810 return function() return R.type_only_module(module_name, a) end, a
811 end
812 return a
813 end)
814end
815
816-- Put already-generated Lua in front of the searcher for one module name.
817--
818-- `package.preload` is searcher position 1 and R.install_searcher puts htl's at 2, so a
819-- preloaded module is never asked of the searcher — which is the point: asking would check
820-- and generate it again. Loaded the same way the searcher would have loaded it, so the
821-- module sees the same chunk name and the same arguments.
822function R.preload_generated(module_name, code, filename)
823 -- Never displace what is already there. The test library and anything a host preloads are
824 -- put in package.preload by whoever owns them, and Lua generated from a `.tl` of the same
825 -- name is not the same module: preloading over `htl.test` gives the file a stand-in whose
826 -- `run()` reports nothing, and every test silently stops counting.
827 if package.preload[module_name] ~= nil then return end
828 local chunk, lerr = load(code, "@" .. filename, "t")
829 if not chunk then
830 error("htl: cached Lua failed to load: " .. tostring(lerr), 0)
831 end
832 package.preload[module_name] = function(modname) return chunk(modname, filename) end
833end
834
835function R.add_path(dir)
836 local templates = dir .. "/?.lua;" .. dir .. "/?/init.lua;" .. dir .. "/?/?.lua"
837 if package.path == nil or package.path == "" then
838 package.path = templates
839 else
840 package.path = templates .. ";" .. package.path
841 end
842end
843
844function R.reset_path()
845 package.path = ""
846end
847
848-- Line coverage: which lines of which chunk ran. Lua's line hook is per thread, so
849-- code that runs inside a coroutine the test creates is not seen.
850local cov = nil
851function R.coverage_start()
852 cov = {}
853 -- The line event is the hot path. One "S" lookup per function (cached by the
854 -- function object) instead of per line; a call/return-event stack was measured
855 -- slower on a call-heavy suite, since calls are almost as frequent as lines there.
856 local srcs = setmetatable({}, { __mode = "k" })
857 local getinfo = debug.getinfo
858 debug.sethook(function(_, line)
859 local fi = getinfo(2, "f")
860 local func = fi and fi.func
861 if func == nil then return end
862 local t = srcs[func]
863 if t == nil then
864 local si = getinfo(2, "S")
865 local src = si and si.source
866 t = false
867 if src then
868 t = cov[src]
869 if not t then
870 t = {}
871 cov[src] = t
872 end
873 end
874 srcs[func] = t
875 end
876 if t then t[line] = true end
877 end, "l")
878end
879
880function R.coverage_stop()
881 debug.sethook()
882 local out = {}
883 for src, lines in pairs(cov or {}) do
884 local list = {}
885 for l in pairs(lines) do list[#list + 1] = l end
886 table.sort(list)
887 out[#out + 1] = { source = src, lines = list }
888 end
889 cov = nil
890 return out
891end
892
893return R
894"#;
895
896impl Htl {
897 pub fn new() -> Result<Self> {
899 let lua = unsafe { Lua::unsafe_new() };
901 Self::from_lua(lua)
902 }
903
904 pub fn with_checker(checker: &Htl) -> Result<Self> {
911 let lua = unsafe { Lua::unsafe_new() };
913 let r: Table = lua
914 .load(RUNTIME_PRELUDE)
915 .set_name("=htl-runtime")
916 .eval()
917 .context("loading htl runtime prelude")?;
918 lua.set_named_registry_value(RUNTIME_REGISTRY_KEY, r)?;
919 lua.set_app_data(CheckerHandle(checker.h.clone()));
920 let begin: Function = checker.h.get("begin_program")?;
921 begin.call::<()>(())?;
922 Ok(Self {
923 lua,
924 h: checker.h.clone(),
925 split: true,
926 })
927 }
928
929 fn runtime(&self) -> Result<Table> {
930 Ok(self
931 .lua
932 .named_registry_value::<Table>(RUNTIME_REGISTRY_KEY)?)
933 }
934
935 pub fn preload_generated(&self, name: &str, code: &str, file: &Path) -> Result<()> {
948 let f: Function = self.runtime()?.get("preload_generated")?;
949 f.call::<()>((name, code, path_str(file)))?;
950 Ok(())
951 }
952
953 pub fn coverage_start(&self) -> Result<()> {
957 let f: Function = self.runtime()?.get("coverage_start")?;
958 f.call::<()>(())?;
959 Ok(())
960 }
961
962 pub fn coverage_stop(&self) -> Result<Vec<(String, Vec<usize>)>> {
965 let f: Function = self.runtime()?.get("coverage_stop")?;
966 let t: Table = f.call(())?;
967 let mut out = Vec::new();
968 for e in t.sequence_values::<Table>() {
969 let e = e?;
970 let source: String = e.get("source")?;
971 let lines: Table = e.get("lines")?;
972 out.push((
973 source,
974 lines
975 .sequence_values::<usize>()
976 .collect::<mlua::Result<_>>()?,
977 ));
978 }
979 Ok(out)
980 }
981
982 pub fn executable_ranges(&self, file: &Path) -> Result<Vec<(usize, usize)>> {
986 Ok(self.coverage_spans(file)?.0)
987 }
988
989 pub fn coverage_spans(&self, file: &Path) -> Result<CoverageSpans> {
993 let f: Function = self.h.get("executable_ranges")?;
994 let (ranges, funcs): (Option<Table>, Option<Table>) = f.call(path_str(file))?;
995 let Some(ranges) = ranges else {
996 return Ok((Vec::new(), Vec::new()));
997 };
998 let mut out = Vec::new();
999 for r in ranges.sequence_values::<Table>() {
1000 let r = r?;
1001 out.push((r.get::<usize>(1)?, r.get::<usize>(2)?));
1002 }
1003 let mut fns = Vec::new();
1004 if let Some(funcs) = funcs {
1005 for f in funcs.sequence_values::<Table>() {
1006 let f = f?;
1007 fns.push(FunctionSpan {
1008 name: f.get("name")?,
1009 line: f.get("y")?,
1010 last: f.get("last")?,
1011 });
1012 }
1013 }
1014 Ok((out, fns))
1015 }
1016
1017 pub fn search_path(&self) -> Result<String> {
1019 let f: Function = self.h.get("get_path")?;
1020 Ok(f.call(())?)
1021 }
1022
1023 pub fn set_search_path(&self, path: &str) -> Result<()> {
1025 let f: Function = self.h.get("set_path")?;
1026 f.call::<()>(path)?;
1027 Ok(())
1028 }
1029
1030 pub fn from_lua(lua: Lua) -> Result<Self> {
1032 let tl_loader: Function = lua
1033 .load(TL_SRC)
1034 .set_name("=tl.lua")
1035 .into_function()
1036 .context("compiling vendored tl.lua")?;
1037 let lint_loader: Function = lua
1038 .load(LINT_SRC)
1039 .set_name("=htl-lint")
1040 .into_function()
1041 .context("compiling htl lint.lua")?;
1042 let package: Table = lua.globals().get("package")?;
1043 let preload: Table = package.get("preload")?;
1044 let fmt_loader: Function = lua
1045 .load(FMT_SRC)
1046 .set_name("=htl-fmt")
1047 .into_function()
1048 .context("compiling htl fmt.lua")?;
1049 preload.set("tl", tl_loader)?;
1050 preload.set("htl.lint", lint_loader)?;
1051 preload.set("htl.fmt", fmt_loader)?;
1052 let h: Table = lua
1053 .load(PRELUDE)
1054 .set_name("=htl-prelude")
1055 .eval()
1056 .context("loading htl prelude")?;
1057 lua.set_named_registry_value(PRELUDE_REGISTRY_KEY, h.clone())?;
1058 let this = Self {
1059 lua,
1060 h,
1061 split: false,
1062 };
1063 this.select_lints(&lint::Selection::default())?;
1067 Ok(this)
1068 }
1069
1070 pub fn lua(&self) -> &Lua {
1071 &self.lua
1072 }
1073
1074 pub fn check(&self, file: &Path) -> Result<CheckInfo> {
1076 let f: Function = self.h.get("check")?;
1077 let t: Table = f.call(path_str(file))?;
1078 read_checkinfo(&t)
1079 }
1080
1081 pub fn check_written(&self, file: &Path) -> Result<CheckInfo> {
1095 let f: Function = self.h.get("check")?;
1096 let opts = self.lua.create_table()?;
1097 opts.set("seed", false)?;
1098 opts.set("store", false)?;
1099 let t: Table = f.call((path_str(file), mlua::Value::Nil, opts))?;
1101 read_checkinfo(&t)
1102 }
1103
1104 pub fn gen_lua(&self, file: &Path) -> Result<(Option<String>, CheckInfo)> {
1106 let f: Function = self.h.get("gen")?;
1107 let (code, t): (Option<String>, Table) = f.call(path_str(file))?;
1108 Ok((code, read_checkinfo(&t)?))
1109 }
1110
1111 pub fn configure_lints(&self, spec: &str) -> Result<()> {
1116 self.select_lints(&lint::Selection::parse(spec)?)
1117 }
1118
1119 pub fn select_lints(&self, sel: &lint::Selection) -> Result<()> {
1127 let t = self.lua.create_table()?;
1128 for (name, on) in sel.of_side(lint::Side::Lua) {
1129 t.set(name, on)?;
1130 }
1131 let tl = self.lua.create_table()?;
1132 for (name, on) in sel.of_side(lint::Side::Tl) {
1133 tl.set(name, on)?;
1134 }
1135 let f: Function = self.h.get("set_lints")?;
1136 f.call::<()>((t, tl))?;
1137 Ok(())
1138 }
1139
1140 pub fn lint_rules(&self) -> Result<Vec<String>> {
1142 Ok(lint::rule_names().into_iter().map(str::to_string).collect())
1143 }
1144
1145 pub fn lua_lint_rules(&self) -> Result<Vec<String>> {
1148 let f: Function = self.h.get("lint_rules")?;
1149 let t: Table = f.call(())?;
1150 Ok(t.sequence_values::<String>().collect::<mlua::Result<_>>()?)
1151 }
1152
1153 pub fn format_file(&self, file: &Path, indent: usize) -> Result<String> {
1155 let f: Function = self.h.get("format")?;
1156 let (out, err): (Option<String>, Option<String>) = f.call((path_str(file), indent))?;
1157 out.ok_or_else(|| anyhow!("{}", err.unwrap_or_else(|| "format failed".into())))
1158 }
1159
1160 pub fn reset_search_path(&self) -> Result<()> {
1163 let f: Function = self.h.get("reset_path")?;
1164 f.call::<()>(())?;
1165 if self.split {
1166 let f: Function = self.runtime()?.get("reset_path")?;
1167 f.call::<()>(())?;
1168 }
1169 Ok(())
1170 }
1171
1172 pub fn add_layout_paths(&self, file: &Path) -> Result<()> {
1177 let dir = parent_dir(file);
1178 let mut dirs = vec![dir.clone()];
1179 if dir.file_name().is_some_and(|n| n == "tests")
1180 && let Some(root) = dir.parent()
1181 {
1182 dirs.push(root.to_path_buf());
1183 let src = root.join("src");
1184 if src.is_dir() {
1185 dirs.push(src);
1186 }
1187 }
1188 self.add_search_paths(&dirs)
1189 }
1190
1191 pub fn add_path(&self, dir: &Path) -> Result<()> {
1193 let f: Function = self.h.get("add_path")?;
1194 f.call::<()>(path_str(dir))?;
1195 if self.split {
1196 let f: Function = self.runtime()?.get("add_path")?;
1198 f.call::<()>(path_str(dir))?;
1199 }
1200 Ok(())
1201 }
1202
1203 pub fn install_searcher(&self) -> Result<()> {
1205 if self.split {
1206 let gen_fn: Function = self.h.get("gen_for_require")?;
1208 let bridge = self.lua.create_function(move |_, name: String| {
1209 let (kind, a, b): (String, Option<String>, Option<String>) = gen_fn.call(name)?;
1210 Ok((kind, a, b))
1211 })?;
1212 let f: Function = self.runtime()?.get("install_searcher")?;
1213 f.call::<()>(bridge)?;
1214 return Ok(());
1215 }
1216 let f: Function = self.h.get("install_searcher")?;
1217 f.call::<()>(())?;
1218 Ok(())
1219 }
1220
1221 pub fn preload(&self, name: &str, lua_src: &str) -> Result<()> {
1229 self.preload_at(name, &module_chunk_name(name), lua_src)
1230 }
1231
1232 pub fn preload_at(&self, name: &str, chunk_name: &str, lua_src: &str) -> Result<()> {
1236 let loader = self
1237 .lua
1238 .load(lua_src)
1239 .set_name(chunk_name)
1240 .into_function()
1241 .with_context(|| format!("compiling preloaded module {name}"))?;
1242 self.preload_table()?.set(name, loader)?;
1243 Ok(())
1244 }
1245
1246 pub fn preload_bytes(&self, name: &str, bytecode: &[u8]) -> Result<()> {
1256 let loader = self
1257 .lua
1258 .load(bytecode)
1259 .set_name(module_chunk_name(name))
1260 .set_mode(ChunkMode::Binary)
1261 .into_function()
1262 .with_context(|| format!("loading bytecode for module {name}"))?;
1263 self.preload_table()?.set(name, loader)?;
1264 Ok(())
1265 }
1266
1267 pub fn exec_bytes(&self, bytecode: &[u8], chunk_name: &str, args: &[String]) -> Result<()> {
1269 let f = self
1270 .lua
1271 .load(bytecode)
1272 .set_name(chunk_name)
1273 .set_mode(ChunkMode::Binary)
1274 .into_function()?;
1275 let va: Variadic<String> = args.iter().cloned().collect();
1276 f.call::<()>(va)?;
1277 Ok(())
1278 }
1279
1280 pub fn preload_value(&self, name: &str, value: impl mlua::IntoLua) -> Result<()> {
1282 let value = value.into_lua(&self.lua)?;
1283 let loader = self.lua.create_function(move |_, ()| Ok(value.clone()))?;
1284 self.preload_table()?.set(name, loader)?;
1285 Ok(())
1286 }
1287
1288 fn preload_table(&self) -> Result<Table> {
1289 let package: Table = self.lua.globals().get("package")?;
1290 Ok(package.get("preload")?)
1291 }
1292
1293 pub fn set_arg(&self, script: &str, args: &[String]) -> Result<()> {
1295 let t = self.lua.create_table()?;
1296 t.set(0, script)?;
1297 for (i, a) in args.iter().enumerate() {
1298 t.set(i + 1, a.as_str())?;
1299 }
1300 self.lua.globals().set("arg", t)?;
1301 Ok(())
1302 }
1303
1304 pub fn exec(&self, lua_src: &str, chunk_name: &str, args: &[String]) -> Result<()> {
1306 let f = self
1307 .lua
1308 .load(lua_src)
1309 .set_name(chunk_name)
1310 .into_function()?;
1311 let va: Variadic<String> = args.iter().cloned().collect();
1312 f.call::<()>(va)?;
1313 Ok(())
1314 }
1315
1316 pub fn run_file(&self, file: &Path, args: &[String]) -> Result<CheckInfo> {
1319 self.add_path(&parent_dir(file))?;
1320 self.install_searcher()?;
1321 self.set_arg(&file.to_string_lossy(), args)?;
1322 let (code, ci) = self.gen_lua(file)?;
1323 let Some(code) = code else { return Ok(ci) };
1324 self.exec(&code, &format!("@{}", file.display()), args)?;
1325 Ok(ci)
1326 }
1327
1328 pub fn compile(&self, name: &str, lua_src: &str) -> Result<Vec<u8>> {
1330 self.compile_with(name, lua_src, true)
1331 }
1332
1333 pub fn compile_with(&self, name: &str, lua_src: &str, strip: bool) -> Result<Vec<u8>> {
1336 let f = self
1337 .lua
1338 .load(lua_src)
1339 .set_name(format!("={name}"))
1340 .into_function()
1341 .with_context(|| format!("compiling generated Lua for {name}"))?;
1342 Ok(f.dump(strip))
1343 }
1344
1345 pub fn fingerprint(&self) -> Result<Vec<u8>> {
1350 let bc = self.compile_with("fp", "return 0", true)?;
1351 Ok(bc.iter().take(31).copied().collect())
1353 }
1354
1355 pub fn lua_requires(&self, src: &str, file: &Path) -> Result<Vec<RequireSite>> {
1357 let f: Function = self.h.get("lua_requires")?;
1358 let t: Table = f.call((src, path_str(file)))?;
1359 read_requires(&t)
1360 }
1361
1362 pub fn resolve_module(&self, name: &str) -> Result<(Option<PathBuf>, Option<PathBuf>)> {
1366 let f: Function = self.h.get("resolve_module")?;
1367 let (found, lua): (Option<String>, Option<String>) = f.call(name)?;
1368 Ok((found.map(PathBuf::from), lua.map(PathBuf::from)))
1369 }
1370
1371 pub fn module_candidates(&self, name: &str) -> Result<Vec<ModuleCandidate>> {
1380 let f: Function = self.h.get("module_candidates")?;
1381 let t: Table = f.call(name)?;
1382 let mut out = Vec::new();
1383 for c in t.sequence_values::<Table>() {
1384 let c = c?;
1385 out.push(ModuleCandidate {
1386 path: PathBuf::from(c.get::<String>("path")?),
1387 kind: ModuleKind::of(&c.get::<String>("kind")?),
1388 dir: PathBuf::from(c.get::<String>("dir")?),
1389 });
1390 }
1391 Ok(out)
1392 }
1393
1394 pub fn search_path_dirs(&self) -> Result<Vec<PathBuf>> {
1397 let f: Function = self.h.get("search_dirs")?;
1398 let t: Table = f.call(())?;
1399 Ok(t.sequence_values::<String>()
1400 .collect::<mlua::Result<Vec<_>>>()?
1401 .into_iter()
1402 .map(PathBuf::from)
1403 .collect())
1404 }
1405
1406 pub fn install_bundle(&self, b: &bundle::Bundle) -> Result<()> {
1408 if b.modules.iter().any(|m| m.kind == bundle::Kind::Bytecode) && !b.fingerprint.is_empty() {
1413 let mine = self.fingerprint()?;
1414 if mine != b.fingerprint {
1415 let built_by = if b.htl_version.is_empty() {
1416 "an htl that did not record its version".to_string()
1417 } else {
1418 format!("htl {}", b.htl_version)
1419 };
1420 bail!(
1421 "bundle bytecode was compiled for {} by {built_by}, but this host runs {} on htl {}; \
1422 rebuild the bundle here, or build it with --source",
1423 bundle::describe_fingerprint(&b.fingerprint),
1424 bundle::describe_fingerprint(&mine),
1425 env!("CARGO_PKG_VERSION")
1426 );
1427 }
1428 }
1429 let package: Table = self.lua.globals().get("package")?;
1432 let preload: Table = package.get("preload")?;
1433 let loaded: Table = package.get("loaded")?;
1434 let missing: Vec<&String> = b
1435 .host_modules
1436 .iter()
1437 .filter(|n| {
1438 matches!(preload.get::<Value>(n.as_str()), Ok(Value::Nil))
1439 && matches!(loaded.get::<Value>(n.as_str()), Ok(Value::Nil))
1440 })
1441 .collect();
1442 if !missing.is_empty() {
1443 bail!(
1444 "bundle expects host-provided module(s) {} (declared only by a .d.tl or [build] host at link \
1445 time): register them with preload / preload_value / htl_preload before running",
1446 missing
1447 .iter()
1448 .map(|m| format!("'{m}'"))
1449 .collect::<Vec<_>>()
1450 .join(", ")
1451 );
1452 }
1453 for m in &b.modules {
1460 if !matches!(preload.get::<Value>(m.name.as_str())?, Value::Nil) {
1461 continue;
1462 }
1463 let payload = m.payload.clone();
1464 let kind = m.kind;
1465 let name = m.name.clone();
1466 let loader =
1467 self.lua
1468 .create_function(move |lua, (modname, origin): (String, Value)| {
1469 let chunk = lua.load(payload.as_slice()).set_name(format!("={name}"));
1470 let f = match kind {
1471 bundle::Kind::Bytecode => {
1472 chunk.set_mode(ChunkMode::Binary).into_function()?
1473 }
1474 bundle::Kind::Source => {
1475 chunk.set_mode(ChunkMode::Text).into_function()?
1476 }
1477 };
1478 f.call::<Value>((modname, origin))
1479 })?;
1480 preload.set(m.name.as_str(), loader)?;
1481 }
1482 Ok(())
1483 }
1484
1485 pub fn run_bundle(&self, b: &bundle::Bundle, args: &[String]) -> Result<()> {
1487 let entry = b
1488 .module(&b.entry)
1489 .cloned()
1490 .ok_or_else(|| anyhow!("entry module '{}' not in bundle", b.entry))?;
1491 self.install_bundle(b)?;
1492 self.set_arg(&b.entry, args)?;
1493 let chunk = self
1494 .lua
1495 .load(entry.payload.as_slice())
1496 .set_name(format!("={}", b.entry));
1497 let main: Function = match entry.kind {
1498 bundle::Kind::Bytecode => chunk.set_mode(ChunkMode::Binary).into_function()?,
1499 bundle::Kind::Source => chunk.set_mode(ChunkMode::Text).into_function()?,
1500 };
1501 let va: Variadic<String> = args.iter().cloned().collect();
1502 main.call::<()>(va)?;
1503 Ok(())
1504 }
1505}
1506
1507fn path_str(p: &Path) -> String {
1508 p.to_string_lossy().into_owned()
1509}
1510
1511fn module_chunk_name(name: &str) -> String {
1515 format!("@{}.tl", name.replace('.', "/"))
1516}
1517
1518pub fn user_message(err: &anyhow::Error) -> String {
1532 if let Some(e) = err.downcast_ref::<mlua::Error>() {
1533 return user_message_lua(e);
1534 }
1535 strip_traceback(&format!("{err:#}"))
1536}
1537
1538pub fn user_message_lua(e: &mlua::Error) -> String {
1541 match e {
1542 mlua::Error::CallbackError { cause, .. } => user_message_lua(cause),
1543 mlua::Error::ExternalError(ext) => ext.to_string(),
1544 mlua::Error::WithContext { cause, .. } => user_message_lua(cause),
1545 other => strip_traceback(&other.to_string()),
1546 }
1547}
1548
1549pub fn developer_message(err: &anyhow::Error) -> String {
1566 let head = user_message(err);
1567 let full = match err.downcast_ref::<mlua::Error>() {
1568 Some(e) => e.to_string(),
1569 None => format!("{err:#}"),
1570 };
1571 match traceback_block(&full) {
1572 Some(tb) => format!("{head}\n{tb}"),
1573 None => head,
1574 }
1575}
1576
1577fn traceback_block(text: &str) -> Option<&str> {
1579 let at = text.find("\nstack traceback:")?;
1580 Some(text[at + 1..].trim_end())
1581}
1582
1583pub fn strip_traceback(text: &str) -> String {
1585 let cut = text.find("\nstack traceback:").unwrap_or(text.len());
1586 text[..cut].trim_end().to_string()
1587}
1588
1589pub fn write_if_changed(path: &Path, text: &str) -> std::io::Result<bool> {
1592 if let Ok(cur) = std::fs::read_to_string(path)
1593 && cur == text
1594 {
1595 return Ok(false);
1596 }
1597 if let Some(dir) = path.parent() {
1598 std::fs::create_dir_all(dir)?;
1599 }
1600 std::fs::write(path, text)?;
1601 Ok(true)
1602}
1603
1604pub fn parent_dir(file: &Path) -> PathBuf {
1606 let dir = file.parent().unwrap_or(Path::new("."));
1607 if dir.as_os_str().is_empty() {
1608 PathBuf::from(".")
1609 } else {
1610 dir.to_path_buf()
1611 }
1612}
1613
1614fn read_checkinfo(t: &Table) -> Result<CheckInfo> {
1615 let seq = |key: &str| -> Result<Vec<String>> {
1616 let inner: Table = t.get(key)?;
1617 Ok(inner
1618 .sequence_values::<String>()
1619 .collect::<mlua::Result<_>>()?)
1620 };
1621 let requires = match t.get::<Table>("requires") {
1622 Ok(list) => read_requires(&list)?,
1623 Err(_) => Vec::new(),
1624 };
1625 let errors = seq("errors")?;
1626 let lints = seq("lints")?;
1627 let error_fixes = read_fixes(t, "error_fixes", errors.len())?;
1628 let lint_fixes = read_fixes(t, "lint_fixes", lints.len())?;
1629 let dependency_errors = match t.get::<Table>("dependency_errors") {
1630 Ok(list) => read_dependency_errors(&list)?,
1631 Err(_) => Vec::new(),
1632 };
1633 Ok(CheckInfo {
1634 errors,
1635 warnings: seq("warnings")?,
1636 deps: seq("deps")?.into_iter().map(PathBuf::from).collect(),
1637 lints,
1638 requires,
1639 error_fixes,
1640 lint_fixes,
1641 dependency_errors,
1642 })
1643}
1644
1645fn read_dependency_errors(list: &Table) -> Result<Vec<DependencyError>> {
1646 let mut out = Vec::new();
1647 for e in list.sequence_values::<Table>() {
1648 let e = e?;
1649 out.push(DependencyError {
1650 file: PathBuf::from(e.get::<String>("file")?),
1651 required_by: PathBuf::from(e.get::<String>("required_by")?),
1652 text: e.get::<String>("text")?,
1653 });
1654 }
1655 Ok(out)
1656}
1657
1658fn read_fixes(t: &Table, key: &str, len: usize) -> Result<Vec<Option<Fix>>> {
1660 let mut out = vec![None; len];
1661 let Ok(list) = t.get::<Table>(key) else {
1662 return Ok(out);
1663 };
1664 for (i, slot) in out.iter_mut().enumerate() {
1665 let v: Value = list.get(i + 1)?;
1666 if let Value::Table(f) = v {
1667 let applicability = match f.get::<Option<String>>("applicability")?.as_deref() {
1668 Some("unsafe") => Applicability::Unsafe,
1669 Some("suggest") => Applicability::Suggest,
1670 _ => Applicability::Safe,
1671 };
1672 let mut edits = Vec::new();
1673 if let Ok(es) = f.get::<Table>("edits") {
1674 for e in es.sequence_values::<Table>() {
1675 let e = e?;
1676 edits.push(Edit {
1677 line: e.get("line")?,
1678 col: e.get("col")?,
1679 end_line: e.get("end_line")?,
1680 end_col: e.get("end_col")?,
1681 text: e.get::<Option<String>>("text")?.unwrap_or_default(),
1682 });
1683 }
1684 }
1685 *slot = Some(Fix {
1686 applicability,
1687 edits,
1688 });
1689 }
1690 }
1691 Ok(out)
1692}
1693
1694fn read_requires(list: &Table) -> Result<Vec<RequireSite>> {
1695 let mut requires = Vec::new();
1696 for r in list.sequence_values::<Table>() {
1697 let r = r?;
1698 requires.push(RequireSite {
1699 module: r.get::<String>("name")?,
1700 path: r.get::<Option<String>>("path")?.map(PathBuf::from),
1701 line: r.get::<Option<usize>>("y")?.unwrap_or(0),
1702 col: r.get::<Option<usize>>("x")?.unwrap_or(0),
1703 });
1704 }
1705 Ok(requires)
1706}
1707
1708pub fn is_tl_source(p: &Path) -> bool {
1710 let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
1711 p.is_file() && name.ends_with(".tl") && !name.ends_with(".d.tl")
1712}
1713
1714pub const DEP_TYPES_NOTE: &str = ".htl-dts";
1717
1718pub fn materialised_types_dirs(types: &Path) -> Vec<PathBuf> {
1727 let Ok(entries) = std::fs::read_dir(types) else {
1728 return Vec::new();
1729 };
1730 let mut out: Vec<PathBuf> = entries
1731 .filter_map(Result::ok)
1732 .map(|e| e.path())
1733 .filter(|p| p.is_dir() && p.join(DEP_TYPES_NOTE).is_file())
1734 .collect();
1735 out.sort();
1736 out
1737}
1738
1739pub fn is_declaration(p: &Path) -> bool {
1741 p.file_name()
1742 .and_then(|s| s.to_str())
1743 .is_some_and(|n| n.ends_with(".d.tl"))
1744}
1745
1746pub const SKIP_DIRS: &[&str] = &["target", "node_modules", ".mlua-pkgs", ".git"];
1749
1750pub fn is_skipped_dir(path: &Path, extra: &[PathBuf]) -> bool {
1754 if !path.is_dir() {
1755 return false;
1756 }
1757 let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
1758 if SKIP_DIRS.contains(&name) || (name.starts_with('.') && name.len() > 1) {
1759 return true;
1760 }
1761 extra.iter().any(|e| same_file(path, e))
1762}
1763
1764pub(crate) fn same_file(a: &Path, b: &Path) -> bool {
1767 match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
1768 (Ok(x), Ok(y)) => x == y,
1769 _ => a == b,
1770 }
1771}
1772
1773#[cfg(feature = "pkg")]
1787pub fn project_skip_dirs(root: &Path) -> Vec<PathBuf> {
1788 match pkg::Project::find(root) {
1789 Some(p) => {
1790 let mut out = vec![p.pkgs_dir];
1791 out.extend(p.vendored_copies);
1792 out
1793 }
1794 None => Vec::new(),
1795 }
1796}
1797
1798#[cfg(not(feature = "pkg"))]
1799pub fn project_skip_dirs(_root: &Path) -> Vec<PathBuf> {
1800 Vec::new()
1801}
1802
1803#[cfg(feature = "pkg")]
1813pub fn patched_dirs(root: &Path) -> Vec<PathBuf> {
1814 match pkg::Project::find(root) {
1815 Some(p) => p.patch_dirs(),
1816 None => Vec::new(),
1817 }
1818}
1819
1820#[cfg(not(feature = "pkg"))]
1821pub fn patched_dirs(_root: &Path) -> Vec<PathBuf> {
1822 Vec::new()
1823}
1824
1825pub fn collect_tl(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
1829 collect_tl_skipping(paths, &[])
1830}
1831
1832pub fn collect_tl_skipping(paths: &[PathBuf], skip: &[PathBuf]) -> Result<Vec<PathBuf>> {
1835 let mut out = Vec::new();
1836 for p in paths {
1837 if p.is_dir() {
1838 let mut extra = project_skip_dirs(p);
1839 extra.extend(skip.iter().cloned());
1840 let root = p.clone();
1841 let walker = walkdir::WalkDir::new(p)
1842 .sort_by_file_name()
1843 .into_iter()
1844 .filter_entry(move |e| e.path() == root || !is_skipped_dir(e.path(), &extra));
1845 for e in walker {
1846 let e = e?;
1847 if is_tl_source(e.path()) {
1848 out.push(e.path().to_path_buf());
1849 }
1850 }
1851 } else if p.is_file() {
1852 out.push(p.clone());
1853 } else {
1854 bail!("no such file or directory: {}", p.display());
1855 }
1856 }
1857 Ok(out)
1858}
1859
1860pub fn module_name(root: &Path, file: &Path) -> Result<String> {
1862 let rel = file.strip_prefix(root)?.with_extension("");
1863 let mut parts: Vec<String> = rel
1864 .components()
1865 .map(|c| c.as_os_str().to_string_lossy().into_owned())
1866 .collect();
1867 if parts.last().map(|s| s == "init").unwrap_or(false) {
1868 parts.pop();
1869 }
1870 if parts.is_empty() {
1871 bail!("cannot derive module name for {}", file.display());
1872 }
1873 Ok(parts.join("."))
1874}