1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
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 has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["list"] }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["list"] }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["list"] }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["list"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["list"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::AllPublic
48 }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 if node.kind() != "list" {
52 return Vec::new();
53 }
54
55 let text = &content[node.byte_range()];
56 let line = node.start_position().row + 1;
57
58 if text.starts_with("(define ") {
60 let rest = &text["(define ".len()..];
61 let name = if rest.starts_with('(') {
62 rest[1..].split_whitespace().next()
64 } else {
65 rest.split_whitespace().next()
67 };
68
69 if let Some(name) = name {
70 let kind = if rest.starts_with('(') || rest.contains("(lambda") {
71 SymbolKind::Function
72 } else {
73 SymbolKind::Variable
74 };
75
76 return vec![Export {
77 name: name.to_string(),
78 kind,
79 line,
80 }];
81 }
82 }
83
84 if text.starts_with("(define-syntax ") {
85 if let Some(name) = text["(define-syntax ".len()..].split_whitespace().next() {
86 return vec![Export {
87 name: name.to_string(),
88 kind: SymbolKind::Function,
89 line,
90 }];
91 }
92 }
93
94 Vec::new()
95 }
96
97 fn scope_creating_kinds(&self) -> &'static [&'static str] {
98 &["list"] }
100
101 fn control_flow_kinds(&self) -> &'static [&'static str] {
102 &["list"] }
104
105 fn complexity_nodes(&self) -> &'static [&'static str] {
106 &["list"]
107 }
108
109 fn nesting_nodes(&self) -> &'static [&'static str] {
110 &["list"]
111 }
112
113 fn signature_suffix(&self) -> &'static str {
114 ""
115 }
116
117 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
118 if node.kind() != "list" {
119 return None;
120 }
121
122 let text = &content[node.byte_range()];
123 let first_line = text.lines().next().unwrap_or(text);
124
125 if text.starts_with("(define ") {
126 let rest = &text["(define ".len()..];
127
128 if rest.starts_with('(') || rest.contains("(lambda") {
130 let name = if rest.starts_with('(') {
131 rest[1..].split_whitespace().next()
132 } else {
133 rest.split_whitespace().next()
134 }?;
135
136 return Some(Symbol {
137 name: name.to_string(),
138 kind: SymbolKind::Function,
139 signature: first_line.trim().to_string(),
140 docstring: None,
141 attributes: Vec::new(),
142 start_line: node.start_position().row + 1,
143 end_line: node.end_position().row + 1,
144 visibility: Visibility::Public,
145 children: Vec::new(),
146 is_interface_impl: false,
147 implements: Vec::new(),
148 });
149 }
150 }
151
152 if text.starts_with("(define-syntax ") {
153 let name = text["(define-syntax ".len()..].split_whitespace().next()?;
154 return Some(Symbol {
155 name: name.to_string(),
156 kind: SymbolKind::Function,
157 signature: first_line.trim().to_string(),
158 docstring: None,
159 attributes: Vec::new(),
160 start_line: node.start_position().row + 1,
161 end_line: node.end_position().row + 1,
162 visibility: Visibility::Public,
163 children: Vec::new(),
164 is_interface_impl: false,
165 implements: Vec::new(),
166 });
167 }
168
169 None
170 }
171
172 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
173 if node.kind() != "list" {
174 return None;
175 }
176
177 let text = &content[node.byte_range()];
178
179 if text.starts_with("(define-library ")
180 || text.starts_with("(library ")
181 || text.starts_with("(module ")
182 {
183 let prefix_len = if text.starts_with("(define-library ") {
184 16
185 } else if text.starts_with("(library ") {
186 9
187 } else {
188 8
189 };
190
191 let name = text[prefix_len..]
192 .split(|c: char| c.is_whitespace() || c == ')')
193 .next()?
194 .to_string();
195
196 return Some(Symbol {
197 name: name.clone(),
198 kind: SymbolKind::Module,
199 signature: format!("(library {})", name),
200 docstring: None,
201 attributes: Vec::new(),
202 start_line: node.start_position().row + 1,
203 end_line: node.end_position().row + 1,
204 visibility: Visibility::Public,
205 children: Vec::new(),
206 is_interface_impl: false,
207 implements: Vec::new(),
208 });
209 }
210
211 None
212 }
213
214 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
215 None
216 }
217 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
218 None
219 }
220
221 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
222 Vec::new()
223 }
224
225 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
226 if node.kind() != "list" {
227 return Vec::new();
228 }
229
230 let text = &content[node.byte_range()];
231 let line = node.start_position().row + 1;
232
233 for prefix in &["(import ", "(require "] {
234 if text.starts_with(prefix) {
235 return vec![Import {
236 module: "import".to_string(),
237 names: Vec::new(),
238 alias: None,
239 is_wildcard: false,
240 is_relative: false,
241 line,
242 }];
243 }
244 }
245
246 Vec::new()
247 }
248
249 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
250 let names_to_use: Vec<&str> = names
252 .map(|n| n.to_vec())
253 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
254 if names_to_use.is_empty() {
255 format!("(import ({}))", import.module)
256 } else {
257 format!(
258 "(import (only ({}) {}))",
259 import.module,
260 names_to_use.join(" ")
261 )
262 }
263 }
264
265 fn is_public(&self, _node: &Node, _content: &str) -> bool {
266 true
267 }
268 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
269 Visibility::Public
270 }
271
272 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
273 let name = symbol.name.as_str();
274 match symbol.kind {
275 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
276 crate::SymbolKind::Module => name == "tests" || name == "test",
277 _ => false,
278 }
279 }
280
281 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
282 None
283 }
284
285 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
286 None
287 }
288 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
289 false
290 }
291 fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
292 None
293 }
294
295 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
296 let ext = path.extension()?.to_str()?;
297 if !["scm", "ss", "rkt"].contains(&ext) {
298 return None;
299 }
300 let stem = path.file_stem()?.to_str()?;
301 Some(stem.to_string())
302 }
303
304 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
305 vec![
306 format!("{}.scm", module),
307 format!("{}.ss", module),
308 format!("{}.rkt", module),
309 ]
310 }
311
312 fn lang_key(&self) -> &'static str {
313 "scheme"
314 }
315
316 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
317 import_name.starts_with("scheme/") || import_name.starts_with("srfi/")
318 }
319
320 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
321 None
322 }
323 fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
324 None
325 }
326 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
327 None
328 }
329 fn get_version(&self, _: &Path) -> Option<String> {
330 None
331 }
332 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
333 None
334 }
335 fn indexable_extensions(&self) -> &'static [&'static str] {
336 &["scm", "ss", "rkt"]
337 }
338 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
339 Vec::new()
340 }
341
342 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
343 use crate::traits::{has_extension, skip_dotfiles};
344 if skip_dotfiles(name) {
345 return true;
346 }
347 !is_dir && !has_extension(name, self.indexable_extensions())
348 }
349
350 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
351 Vec::new()
352 }
353
354 fn package_module_name(&self, entry_name: &str) -> String {
355 entry_name
356 .strip_suffix(".scm")
357 .or_else(|| entry_name.strip_suffix(".ss"))
358 .or_else(|| entry_name.strip_suffix(".rkt"))
359 .unwrap_or(entry_name)
360 .to_string()
361 }
362
363 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
364 if path.is_file() {
365 Some(path.to_path_buf())
366 } else {
367 None
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::validate_unused_kinds_audit;
376
377 #[test]
378 fn unused_node_kinds_audit() {
379 #[rustfmt::skip]
380 let documented_unused: &[&str] = &[
381 "block_comment",
382 ];
383 validate_unused_kinds_audit(&Scheme, documented_unused)
384 .expect("Scheme unused node kinds audit failed");
385 }
386}