normalize_languages/
scheme.rs1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub struct Scheme;
10
11impl Language for Scheme {
12 fn name(&self) -> &'static str {
13 "Scheme"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["scm", "ss", "rkt"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "scheme"
20 }
21
22 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27 if node.kind() != "list" {
28 return Vec::new();
29 }
30
31 let text = &content[node.byte_range()];
32 let line = node.start_position().row + 1;
33
34 for prefix in &["(import ", "(require "] {
35 if text.starts_with(prefix) {
36 return vec![Import {
37 module: "import".to_string(),
38 names: Vec::new(),
39 alias: None,
40 is_wildcard: false,
41 is_relative: false,
42 line,
43 }];
44 }
45 }
46
47 Vec::new()
48 }
49
50 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
51 let names_to_use: Vec<&str> = names
53 .map(|n| n.to_vec())
54 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
55 if names_to_use.is_empty() {
56 format!("(import ({}))", import.module)
57 } else {
58 format!(
59 "(import (only ({}) {}))",
60 import.module,
61 names_to_use.join(" ")
62 )
63 }
64 }
65
66 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
67 let name = symbol.name.as_str();
68 match symbol.kind {
69 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
70 crate::SymbolKind::Module => name == "tests" || name == "test",
71 _ => false,
72 }
73 }
74
75 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
76 Some(*node)
78 }
79 fn analyze_container_body(
80 &self,
81 body_node: &Node,
82 content: &str,
83 inner_indent: &str,
84 ) -> Option<ContainerBody> {
85 crate::body::analyze_paren_body(body_node, content, inner_indent)
86 }
87
88 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
89 if node.kind() != "list" {
95 return node
96 .child_by_field_name("name")
97 .map(|n| &content[n.byte_range()]);
98 }
99 let mut cursor = node.walk();
100 let mut seen_define = false;
101 for child in node.children(&mut cursor) {
102 match child.kind() {
103 "symbol" if !seen_define => {
104 seen_define = true;
106 }
107 "symbol" if seen_define => {
108 return Some(&content[child.byte_range()]);
110 }
111 "list" if seen_define => {
112 let mut inner_cursor = child.walk();
115 for inner in child.children(&mut inner_cursor) {
116 if inner.kind() == "symbol" {
117 return Some(&content[inner.byte_range()]);
118 }
119 }
120 return None;
121 }
122 _ => {}
123 }
124 }
125 None
126 }
127
128 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
129 static RESOLVER: SchemeModuleResolver = SchemeModuleResolver;
130 Some(&RESOLVER)
131 }
132}
133
134impl LanguageSymbols for Scheme {}
135
136pub struct SchemeModuleResolver;
146
147impl ModuleResolver for SchemeModuleResolver {
148 fn workspace_config(&self, root: &Path) -> ResolverConfig {
149 ResolverConfig {
150 workspace_root: root.to_path_buf(),
151 path_mappings: Vec::new(),
152 search_roots: vec![root.to_path_buf()],
153 }
154 }
155
156 fn module_of_file(&self, root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
157 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
158 if ext != "scm" && ext != "ss" && ext != "sld" {
159 return Vec::new();
160 }
161 if let Ok(rel) = file.strip_prefix(root) {
162 let rel_str = rel.to_str().unwrap_or("");
163 let module = rel_str
164 .trim_end_matches(".sld")
165 .trim_end_matches(".scm")
166 .trim_end_matches(".ss")
167 .replace('/', " ");
168 if !module.is_empty() {
169 return vec![ModuleId {
170 canonical_path: format!("({})", module),
171 }];
172 }
173 }
174 Vec::new()
175 }
176
177 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
178 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
179 if ext != "scm" && ext != "ss" && ext != "sld" && ext != "rkt" {
180 return Resolution::NotApplicable;
181 }
182 let raw = &spec.raw;
183
184 if raw == "import" || raw == "require" {
186 return Resolution::NotFound;
187 }
188
189 let normalized = raw.trim_start_matches('(').trim_end_matches(')');
191 let path_part = normalized.replace(' ', "/");
192 let exported_name = normalized
193 .rsplit(' ')
194 .next()
195 .unwrap_or(normalized)
196 .to_string();
197
198 for ext_try in &["sld", "scm", "ss"] {
199 let candidate = cfg
200 .workspace_root
201 .join(format!("{}.{}", path_part, ext_try));
202 if candidate.exists() {
203 return Resolution::Resolved(candidate, exported_name.clone());
204 }
205 }
206 Resolution::NotFound
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::validate_unused_kinds_audit;
214
215 #[test]
216 fn unused_node_kinds_audit() {
217 #[rustfmt::skip]
218 let documented_unused: &[&str] = &[
219 "block_comment",
220 ];
221 validate_unused_kinds_audit(&Scheme, documented_unused)
222 .expect("Scheme unused node kinds audit failed");
223 }
224}