normalize_manifest/
common_lisp.rs1use crate::sexpr::Sexp;
8use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
9
10pub struct AsdParser;
16
17impl ManifestParser for AsdParser {
18 fn filename(&self) -> &'static str {
19 "*.asd"
20 }
21
22 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
23 let mut name: Option<String> = None;
24 let mut version: Option<String> = None;
25 let mut deps: Vec<DeclaredDep> = Vec::new();
26
27 for token in &Sexp::parse(content) {
28 let Some(children) = token.as_list() else {
29 continue;
30 };
31 let head = match children.first() {
32 Some(Sexp::Atom(s)) => s.as_str(),
33 _ => continue,
34 };
35 if head == "asdf:defsystem" || head == "defsystem" || head == "asdf/defsystem:defsystem"
36 {
37 parse_defsystem(children, &mut name, &mut version, &mut deps);
38 }
39 }
40
41 Ok(ParsedManifest {
42 ecosystem: "quicklisp",
43 name,
44 version,
45 dependencies: deps,
46 })
47 }
48}
49
50fn parse_defsystem(
51 children: &[Sexp],
52 name: &mut Option<String>,
53 version: &mut Option<String>,
54 deps: &mut Vec<DeclaredDep>,
55) {
56 if name.is_none()
58 && let Some(sys_name) = children.get(1).and_then(|t| t.as_text())
59 {
60 *name = Some(strip_hash_colon(sys_name));
61 }
62
63 let mut i = 2;
64 while i < children.len() {
65 if let Sexp::Atom(kw) = &children[i] {
66 match kw.to_lowercase().as_str() {
67 ":version" => {
68 if version.is_none()
69 && let Some(v) = children.get(i + 1).and_then(|t| t.as_text())
70 {
71 *version = Some(v.to_string());
72 }
73 i += 2;
74 continue;
75 }
76 ":depends-on" => {
77 if let Some(dep_list) = children.get(i + 1).and_then(|t| t.as_list()) {
78 parse_depends_on(dep_list, deps);
79 }
80 i += 2;
81 continue;
82 }
83 _ => {}
84 }
85 }
86 i += 1;
87 }
88}
89
90fn parse_depends_on(dep_list: &[Sexp], deps: &mut Vec<DeclaredDep>) {
91 for token in dep_list {
92 match token {
93 Sexp::Atom(s) => {
94 let dep_name = strip_hash_colon(s);
95 if !dep_name.is_empty() {
96 deps.push(DeclaredDep {
97 name: dep_name,
98 version_req: None,
99 kind: DepKind::Normal,
100 });
101 }
102 }
103 Sexp::Str(s) => {
104 if !s.is_empty() {
105 deps.push(DeclaredDep {
106 name: s.clone(),
107 version_req: None,
108 kind: DepKind::Normal,
109 });
110 }
111 }
112 Sexp::List(sub) => {
113 if let Some(dep) = parse_dep_form(sub) {
114 deps.push(dep);
115 }
116 }
117 }
118 }
119}
120
121fn parse_dep_form(sub: &[Sexp]) -> Option<DeclaredDep> {
126 let head = match sub.first() {
127 Some(Sexp::Atom(s)) => s.to_lowercase(),
128 _ => return None,
129 };
130
131 match head.as_str() {
132 ":version" => {
133 let dep_name = sub.get(1)?.as_text().map(strip_hash_colon)?;
135 if dep_name.is_empty() {
136 return None;
137 }
138 let version_req = sub.get(2).and_then(|t| t.as_text()).map(|s| s.to_string());
139 Some(DeclaredDep {
140 name: dep_name,
141 version_req,
142 kind: DepKind::Normal,
143 })
144 }
145 ":feature" => {
146 let dep_name = sub.iter().rev().find_map(|t| {
149 if let Sexp::Atom(s) = t {
150 let stripped = strip_hash_colon(s);
151 if !stripped.is_empty() && !stripped.starts_with(':') {
152 return Some(stripped);
153 }
154 }
155 None
156 })?;
157 Some(DeclaredDep {
158 name: dep_name,
159 version_req: None,
160 kind: DepKind::Optional,
161 })
162 }
163 _ => None,
164 }
165}
166
167fn strip_hash_colon(s: &str) -> String {
168 s.trim_start_matches('#')
169 .trim_start_matches(':')
170 .to_string()
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::ManifestParser;
177
178 const SAMPLE: &str = r#"(asdf:defsystem #:my-system
179 :name "my-system"
180 :version "1.0.0"
181 :description "My CL system"
182 :depends-on (#:alexandria
183 #:cl-ppcre
184 (:version #:bordeaux-threads "0.8.0")
185 (:feature :sbcl #:sb-posix))
186 :in-order-to ((test-op (test-op #:my-system/tests))))
187
188(asdf:defsystem #:my-system/tests
189 :depends-on (#:my-system #:fiveam))
190"#;
191
192 #[test]
193 fn test_parse_asd() {
194 let m = AsdParser.parse(SAMPLE).unwrap();
195 assert_eq!(m.ecosystem, "quicklisp");
196 assert_eq!(m.name.as_deref(), Some("my-system"));
197 assert_eq!(m.version.as_deref(), Some("1.0.0"));
198
199 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
200 assert!(names.contains(&"alexandria"), "{names:?}");
201 assert!(names.contains(&"cl-ppcre"), "{names:?}");
202 assert!(names.contains(&"bordeaux-threads"), "{names:?}");
203 assert!(names.contains(&"sb-posix"), "{names:?}");
204
205 let bt = m
206 .dependencies
207 .iter()
208 .find(|d| d.name == "bordeaux-threads")
209 .unwrap();
210 assert_eq!(bt.version_req.as_deref(), Some("0.8.0"));
211 assert_eq!(bt.kind, DepKind::Normal);
212
213 let sbposix = m
214 .dependencies
215 .iter()
216 .find(|d| d.name == "sb-posix")
217 .unwrap();
218 assert_eq!(sbposix.kind, DepKind::Optional);
219 }
220
221 #[test]
222 fn test_multiple_systems_deps_merged() {
223 let m = AsdParser.parse(SAMPLE).unwrap();
226 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
227 assert!(names.contains(&"fiveam"), "{names:?}");
229 }
230}