Skip to main content

leekscript_rs/
signatures.rs

1//! Loading of signature files (.sig) for stdlib and API definitions.
2//!
3//! Use [`load_signatures_from_dir`] or [`default_signature_roots`] to obtain
4//! parsed signature roots for [`DocumentAnalysis`](crate::DocumentAnalysis) and
5//! [`analyze_with_signatures`].
6
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use sipha::line_index::LineIndex;
12use sipha::red::SyntaxNode;
13
14use crate::parse_signatures;
15use leekscript_core::syntax::Kind;
16
17/// Default directory for .sig files when no explicit path is given.
18/// Override with env var `LEEKSCRIPT_SIGNATURES_DIR`.
19pub const DEFAULT_SIGNATURES_DIR: &str = "examples/signatures";
20
21/// Load signature AST roots from a directory (all `*.sig` files).
22#[must_use]
23pub fn load_signatures_from_dir(dir: &Path) -> Vec<SyntaxNode> {
24    let mut roots = Vec::new();
25    let Ok(entries) = fs::read_dir(dir) else {
26        return roots;
27    };
28    let mut files: Vec<_> = entries
29        .filter_map(std::result::Result::ok)
30        .map(|e| e.path())
31        .filter(|p| p.is_file() && p.extension().is_some_and(|e| e == "sig"))
32        .collect();
33    files.sort_by_key(|p| p.as_os_str().to_owned());
34    for path in files {
35        if let Ok(s) = fs::read_to_string(&path) {
36            if let Ok(Some(node)) = parse_signatures(&s) {
37                roots.push(node);
38            }
39        }
40    }
41    roots
42}
43
44/// If no signature path was given, use default locations: `LEEKSCRIPT_SIGNATURES_DIR` env var
45/// if set, else `examples/signatures`, else `leekscript-rs/examples/signatures` (when run from
46/// workspace root). Returns empty vec if no directory is found.
47#[must_use]
48pub fn default_signature_roots() -> Vec<SyntaxNode> {
49    default_signature_roots_with_locations().0
50}
51
52/// Build a map from function/global name to (path, 0-based line) for one parsed .sig file.
53#[must_use]
54pub fn build_sig_definition_locations(
55    path: PathBuf,
56    source: &str,
57    root: &SyntaxNode,
58) -> HashMap<String, (PathBuf, u32)> {
59    let mut out = HashMap::new();
60    let line_index = LineIndex::new(source.as_bytes());
61    let file_nodes: Vec<SyntaxNode> = if root.kind_as::<Kind>() == Some(Kind::NodeSigFile) {
62        vec![root.clone()]
63    } else {
64        root.children()
65            .filter_map(|c| c.as_node().cloned())
66            .filter(|n| n.kind_as::<Kind>() == Some(Kind::NodeSigFile))
67            .collect()
68    };
69    for file in file_nodes {
70        for child in file.children().filter_map(|c| c.as_node().cloned()) {
71            if child.kind_as::<Kind>() == Some(Kind::NodeSigFunction)
72                || child.kind_as::<Kind>() == Some(Kind::NodeSigGlobal)
73            {
74                let name = child
75                    .descendant_tokens()
76                    .into_iter()
77                    .find(|t| t.kind_as::<Kind>() == Some(Kind::TokIdent))
78                    .map(|t| t.text().to_string());
79                if let Some(name) = name {
80                    let start = child.text_range().start;
81                    let (line, _) = line_index.line_col(start as u32);
82                    out.insert(name, (path.clone(), line));
83                }
84            }
85        }
86    }
87    out
88}
89
90/// Like [`default_signature_roots`] but also returns a map from function/global name to (path, 0-based line)
91/// for hover/definition links into .sig files.
92#[must_use]
93pub fn default_signature_roots_with_locations() -> (Vec<SyntaxNode>, HashMap<String, (PathBuf, u32)>)
94{
95    let candidates: Vec<PathBuf> =
96        if let Some(ref d) = std::env::var_os("LEEKSCRIPT_SIGNATURES_DIR") {
97            vec![d.into()]
98        } else {
99            vec![
100                PathBuf::from(DEFAULT_SIGNATURES_DIR),
101                PathBuf::from("leekscript-rs/examples/signatures"),
102            ]
103        };
104    for dir in candidates {
105        if dir.is_dir() {
106            let (roots, locations) = load_signatures_from_dir_with_locations(&dir);
107            if !roots.is_empty() {
108                return (roots, locations);
109            }
110        }
111    }
112    (Vec::new(), HashMap::new())
113}
114
115/// Load signature roots and a map from function/global name to (path, 0-based line) for link resolution.
116#[must_use]
117pub fn load_signatures_from_dir_with_locations(
118    dir: &Path,
119) -> (Vec<SyntaxNode>, HashMap<String, (PathBuf, u32)>) {
120    let mut roots = Vec::new();
121    let mut locations = HashMap::new();
122    let Ok(entries) = fs::read_dir(dir) else {
123        return (roots, locations);
124    };
125    let mut files: Vec<_> = entries
126        .filter_map(std::result::Result::ok)
127        .map(|e| e.path())
128        .filter(|p| p.is_file() && p.extension().is_some_and(|e| e == "sig"))
129        .collect();
130    files.sort_by_key(|p| p.as_os_str().to_owned());
131    for path in files {
132        if let Ok(s) = fs::read_to_string(&path) {
133            if let Ok(Some(node)) = parse_signatures(&s) {
134                let path_buf = path.to_path_buf();
135                for (name, (_, line)) in build_sig_definition_locations(path_buf.clone(), &s, &node)
136                {
137                    locations.insert(name, (path_buf.clone(), line));
138                }
139                roots.push(node);
140            }
141        }
142    }
143    (roots, locations)
144}