1use std::collections::HashMap;
5use std::path::Path;
6
7use crate::parser_warn as warn;
8use crate::parsers::utils::{
9 MAX_ITERATION_COUNT, RecursionGuard, read_file_to_string, truncate_field,
10};
11use packageurl::PackageUrl;
12use serde_json::Value as JsonValue;
13
14use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
15
16use super::PackageParser;
17
18pub struct ClojureDepsEdnParser;
19
20impl PackageParser for ClojureDepsEdnParser {
21 const PACKAGE_TYPE: PackageType = PackageType::Maven;
22
23 fn is_match(path: &Path) -> bool {
24 path.file_name().is_some_and(|name| name == "deps.edn")
25 }
26
27 fn extract_packages(path: &Path) -> Vec<PackageData> {
28 let content = match read_file_to_string(path, None) {
29 Ok(content) => content,
30 Err(error) => {
31 warn!("Failed to read deps.edn at {:?}: {}", path, error);
32 return vec![default_package_data(Some(DatasourceId::ClojureDepsEdn))];
33 }
34 };
35
36 match parse_forms(&content)
37 .and_then(|forms| {
38 forms
39 .into_iter()
40 .next()
41 .ok_or_else(|| "deps.edn contained no readable forms".to_string())
42 })
43 .and_then(|form| parse_deps_edn_form(&form))
44 {
45 Ok(package) => vec![package],
46 Err(error) => {
47 warn!("Failed to parse deps.edn at {:?}: {}", path, error);
48 vec![default_package_data(Some(DatasourceId::ClojureDepsEdn))]
49 }
50 }
51 }
52}
53
54pub struct ClojureProjectCljParser;
55
56impl PackageParser for ClojureProjectCljParser {
57 const PACKAGE_TYPE: PackageType = PackageType::Maven;
58
59 fn is_match(path: &Path) -> bool {
60 path.file_name().is_some_and(|name| name == "project.clj")
61 }
62
63 fn extract_packages(path: &Path) -> Vec<PackageData> {
64 let content = match read_file_to_string(path, None) {
65 Ok(content) => content,
66 Err(error) => {
67 warn!("Failed to read project.clj at {:?}: {}", path, error);
68 return vec![default_package_data(Some(DatasourceId::ClojureProjectClj))];
69 }
70 };
71
72 if looks_like_template_project_clj(&content) {
73 return vec![default_package_data(Some(DatasourceId::ClojureProjectClj))];
74 }
75
76 if !content.contains("(defproject") {
77 return vec![default_package_data(Some(DatasourceId::ClojureProjectClj))];
78 }
79
80 let forms = match parse_forms(&content) {
81 Ok(forms) => forms,
82 Err(error) => {
83 warn!("Failed to parse project.clj at {:?}: {}", path, error);
84 return vec![default_package_data(Some(DatasourceId::ClojureProjectClj))];
85 }
86 };
87
88 let Some(form) = forms.into_iter().find(|form| {
89 matches!(
90 form,
91 Form::List(items) if matches!(items.first(), Some(Form::Symbol(symbol)) if symbol == "defproject")
92 )
93 }) else {
94 return vec![default_package_data(Some(DatasourceId::ClojureProjectClj))];
95 };
96
97 match parse_project_clj_form(&form) {
98 Ok(package) => vec![package],
99 Err(error) => {
100 warn!("Failed to parse project.clj at {:?}: {}", path, error);
101 vec![default_package_data(Some(DatasourceId::ClojureProjectClj))]
102 }
103 }
104 }
105}
106
107#[derive(Clone, Debug)]
108enum Form {
109 Nil,
110 Bool(bool),
111 String(String),
112 Keyword(String),
113 Symbol(String),
114 Vector(Vec<Form>),
115 List(Vec<Form>),
116 Map(Vec<(Form, Form)>),
117 Prefixed(Box<Form>),
118}
119
120struct Reader {
121 chars: Vec<char>,
122 index: usize,
123 guard: RecursionGuard<()>,
124}
125
126impl Reader {
127 fn new(input: &str) -> Self {
128 Self {
129 chars: input.chars().collect(),
130 index: 0,
131 guard: RecursionGuard::depth_only(),
132 }
133 }
134
135 fn parse_all(mut self) -> Result<Vec<Form>, String> {
136 let mut forms = Vec::new();
137 let mut count = 0usize;
138 while self.skip_ws_and_comments() {
139 count += 1;
140 if count > MAX_ITERATION_COUNT {
141 warn!("Reached MAX_ITERATION_COUNT in parse_all, stopping early");
142 break;
143 }
144 forms.push(self.parse_form()?);
145 }
146 Ok(forms)
147 }
148
149 fn skip_ws_and_comments(&mut self) -> bool {
150 loop {
151 while self
152 .peek()
153 .is_some_and(|ch| ch.is_whitespace() || ch == ',')
154 {
155 self.index += 1;
156 }
157 if self.peek() == Some(';') {
158 while let Some(ch) = self.peek() {
159 self.index += 1;
160 if ch == '\n' {
161 break;
162 }
163 }
164 continue;
165 }
166 return self.peek().is_some();
167 }
168 }
169
170 fn parse_form(&mut self) -> Result<Form, String> {
171 if self.guard.descend() {
172 return Err("recursion depth exceeded".to_string());
173 }
174 self.skip_ws_and_comments();
175 let result = match self.peek() {
176 Some('"') => self.parse_string().map(Form::String),
177 Some(':') => self.parse_keyword().map(Form::Keyword),
178 Some('[') => self.parse_collection('[', ']').map(Form::Vector),
179 Some('(') => self.parse_collection('(', ')').map(Form::List),
180 Some('{') => self.parse_map(),
181 Some('^') => {
182 self.index += 1;
183 let _ = self.parse_form()?;
184 let result = self.parse_form();
185 self.guard.ascend();
186 return result;
187 }
188 Some('~') | Some('\'') | Some('`') | Some('@') => {
189 self.index += 1;
190 let form = self.parse_form()?;
191 self.guard.ascend();
192 return Ok(Form::Prefixed(Box::new(form)));
193 }
194 Some('#') => {
195 let result = self.parse_dispatch_form();
196 self.guard.ascend();
197 return result;
198 }
199 Some(_) => self.parse_atom(),
200 None => Err("unexpected end of input".to_string()),
201 };
202 self.guard.ascend();
203 result
204 }
205
206 fn parse_dispatch_form(&mut self) -> Result<Form, String> {
207 self.expect('#')?;
208 match self.peek() {
209 Some('_') => {
210 self.index += 1;
211 let _ = self.parse_form()?;
212 self.parse_form()
213 }
214 Some('=') => Err("unsupported reader eval dispatch".to_string()),
215 Some('\'') => {
216 self.index += 1;
217 let form = self.parse_form()?;
218 Ok(Form::Prefixed(Box::new(form)))
219 }
220 Some('"') => {
221 self.parse_string().map(Form::String)
223 }
224 Some('{') => {
225 self.parse_collection('{', '}').map(Form::Vector)
227 }
228 Some('(') => {
229 self.parse_collection('(', ')').map(Form::List)
231 }
232 Some('?') => {
233 self.index += 1;
236 if self.peek() == Some('@') {
237 self.index += 1;
238 }
239 let _ = self.parse_form()?;
240 self.parse_form()
241 }
242 Some(ch) if !is_delimiter(ch) => {
243 let _ = self.parse_atom()?;
246 self.parse_form()
247 }
248 Some(ch) => Err(format!("unsupported reader dispatch '#{ch}'")),
249 None => Err("unexpected end of input after '#'".to_string()),
250 }
251 }
252
253 fn parse_string(&mut self) -> Result<String, String> {
254 self.expect('"')?;
255 let mut result = String::new();
256 let mut escaped = false;
257 while let Some(ch) = self.peek() {
258 self.index += 1;
259 if escaped {
260 result.push(match ch {
261 'n' => '\n',
262 'r' => '\r',
263 't' => '\t',
264 '"' => '"',
265 '\\' => '\\',
266 other => other,
267 });
268 escaped = false;
269 } else if ch == '\\' {
270 escaped = true;
271 } else if ch == '"' {
272 return Ok(result);
273 } else {
274 result.push(ch);
275 }
276 }
277 Err("unterminated string".to_string())
278 }
279
280 fn parse_keyword(&mut self) -> Result<String, String> {
281 self.expect(':')?;
282 let start = self.index;
283 while let Some(ch) = self.peek() {
284 if is_delimiter(ch) {
285 break;
286 }
287 self.index += 1;
288 }
289 if self.index == start {
290 return Err("empty keyword".to_string());
291 }
292 Ok(self.chars[start..self.index].iter().collect())
293 }
294
295 fn parse_collection(&mut self, open: char, close: char) -> Result<Vec<Form>, String> {
296 self.expect(open)?;
297 let mut forms = Vec::new();
298 let mut count = 0usize;
299 loop {
300 self.skip_ws_and_comments();
301 if self.peek() == Some(close) {
302 self.index += 1;
303 return Ok(forms);
304 }
305 if self.peek().is_none() {
306 return Err(format!("unterminated collection starting with {open}"));
307 }
308 count += 1;
309 if count > MAX_ITERATION_COUNT {
310 warn!("Reached MAX_ITERATION_COUNT in parse_collection, stopping early");
311 break;
312 }
313 forms.push(self.parse_form()?);
314 }
315 Ok(forms)
316 }
317
318 fn parse_map(&mut self) -> Result<Form, String> {
319 self.expect('{')?;
320 let mut entries = Vec::new();
321 let mut count = 0usize;
322 loop {
323 self.skip_ws_and_comments();
324 if self.peek() == Some('}') {
325 self.index += 1;
326 return Ok(Form::Map(entries));
327 }
328 if self.peek().is_none() {
329 return Err("unterminated map".to_string());
330 }
331 count += 1;
332 if count > MAX_ITERATION_COUNT {
333 warn!("Reached MAX_ITERATION_COUNT in parse_map, stopping early");
334 break;
335 }
336 let key = self.parse_form()?;
337 self.skip_ws_and_comments();
338 if self.peek() == Some('}') {
339 return Err("map missing value".to_string());
340 }
341 let value = self.parse_form()?;
342 entries.push((key, value));
343 }
344 Ok(Form::Map(entries))
345 }
346
347 fn parse_atom(&mut self) -> Result<Form, String> {
348 let start = self.index;
349 while let Some(ch) = self.peek() {
350 if is_delimiter(ch) {
351 break;
352 }
353 self.index += 1;
354 }
355 let token: String = self.chars[start..self.index].iter().collect();
356 if token.is_empty() {
357 return Err("empty token".to_string());
358 }
359 Ok(match token.as_str() {
360 "nil" => Form::Nil,
361 "true" => Form::Bool(true),
362 "false" => Form::Bool(false),
363 _ => Form::Symbol(token),
364 })
365 }
366
367 fn expect(&mut self, expected: char) -> Result<(), String> {
368 match self.peek() {
369 Some(ch) if ch == expected => {
370 self.index += 1;
371 Ok(())
372 }
373 Some(ch) => Err(format!("expected '{expected}', found '{ch}'")),
374 None => Err(format!("expected '{expected}', found end of input")),
375 }
376 }
377
378 fn peek(&self) -> Option<char> {
379 self.chars.get(self.index).copied()
380 }
381}
382
383fn is_delimiter(ch: char) -> bool {
384 ch.is_whitespace()
385 || ch == ','
386 || matches!(
387 ch,
388 '[' | ']' | '{' | '}' | '(' | ')' | '"' | ';' | '\'' | '`' | '~' | '@'
389 )
390}
391
392fn parse_forms(input: &str) -> Result<Vec<Form>, String> {
393 Reader::new(input).parse_all()
394}
395
396fn parse_deps_edn_form(form: &Form) -> Result<PackageData, String> {
397 let Form::Map(entries) = form else {
398 return Err("deps.edn root is not a map".to_string());
399 };
400
401 let mut package = default_package_data(Some(DatasourceId::ClojureDepsEdn));
402 let mut dependencies = Vec::new();
403 let mut extra_data = HashMap::new();
404
405 if let Some(Form::Map(dep_map)) = map_get_keyword(entries, "deps") {
406 dependencies.extend(extract_deps_map(dep_map, None, true));
407 }
408
409 if let Some(Form::Map(alias_map)) = map_get_keyword(entries, "aliases") {
410 for (alias_key, alias_value) in alias_map {
411 let Some(alias_name) = keyword_or_symbol_name(alias_key) else {
412 continue;
413 };
414 let Form::Map(alias_entries) = alias_value else {
415 continue;
416 };
417 for dep_key in [
418 "extra-deps",
419 "override-deps",
420 "default-deps",
421 "deps",
422 "replace-deps",
423 ] {
424 if let Some(Form::Map(dep_map)) = map_get_keyword(alias_entries, dep_key) {
425 dependencies.extend(extract_deps_map(dep_map, Some(&alias_name), false));
426 }
427 }
428 }
429 if let Some(json) = form_to_json(
430 &Form::Map(alias_map.clone()),
431 &mut RecursionGuard::depth_only(),
432 ) {
433 extra_data.insert("aliases".to_string(), json);
434 }
435 }
436
437 if let Some(value) = map_get_keyword(entries, "paths")
438 .and_then(|f| form_to_json(f, &mut RecursionGuard::depth_only()))
439 {
440 extra_data.insert("paths".to_string(), value);
441 }
442 if let Some(value) = map_get_keyword(entries, "mvn/repos")
443 .and_then(|f| form_to_json(f, &mut RecursionGuard::depth_only()))
444 {
445 extra_data.insert("mvn_repos".to_string(), value);
446 }
447
448 package.dependencies = dependencies;
449 package.extra_data = (!extra_data.is_empty()).then_some(extra_data);
450 Ok(package)
451}
452
453fn parse_project_clj_form(form: &Form) -> Result<PackageData, String> {
454 let Form::List(items) = form else {
455 return Err("project.clj root is not a list".to_string());
456 };
457 if !matches!(items.first(), Some(Form::Symbol(symbol)) if symbol == "defproject") {
458 return Err("project.clj root is not defproject".to_string());
459 }
460
461 let Some((namespace, name)) = items.get(1).and_then(parse_lib_form) else {
462 return Err("defproject missing project identifier".to_string());
463 };
464 let Some(version) = items.get(2).and_then(form_as_string) else {
465 return Err("defproject missing project version".to_string());
466 };
467
468 let mut package = default_package_data(Some(DatasourceId::ClojureProjectClj));
469 package.namespace = namespace.clone().map(truncate_field);
470 package.name = Some(truncate_field(name.clone()));
471 package.version = Some(truncate_field(version.to_string()));
472 package.purl = build_maven_purl(namespace.as_deref(), &name, Some(version)).map(truncate_field);
473
474 let mut index = 3usize;
475 while index + 1 < items.len() {
476 let Some(key) = form_as_keyword(&items[index]) else {
477 index += 1;
478 continue;
479 };
480 let value = &items[index + 1];
481
482 match key {
483 "description" => {
484 package.description = form_as_string(value).map(|s| truncate_field(s.to_owned()))
485 }
486 "url" => {
487 package.homepage_url = form_as_string(value).map(|s| truncate_field(s.to_owned()))
488 }
489 "license" => {
490 package.extracted_license_statement =
491 format_license(value, &mut RecursionGuard::depth_only()).map(truncate_field);
492 }
493 "scm" => {
494 if let Form::Map(entries) = value {
495 package.vcs_url = map_get_keyword(entries, "url")
496 .and_then(form_as_string)
497 .map(|s| truncate_field(s.to_owned()));
498 }
499 }
500 "dependencies" => {
501 if let Form::Vector(deps) = value {
502 package
503 .dependencies
504 .extend(extract_project_dependencies(deps, None));
505 }
506 }
507 "profiles" => {
508 if let Form::Map(entries) = value {
509 for (profile_key, profile_value) in entries {
510 let Some(profile_name) = keyword_or_symbol_name(profile_key) else {
511 continue;
512 };
513 let Form::Map(profile_entries) = profile_value else {
514 continue;
515 };
516 if let Some(Form::Vector(deps)) =
517 map_get_keyword(profile_entries, "dependencies")
518 {
519 package
520 .dependencies
521 .extend(extract_project_dependencies(deps, Some(&profile_name)));
522 }
523 }
524 }
525 }
526 _ => {}
527 }
528 index += 2;
529 }
530
531 Ok(package)
532}
533
534fn extract_deps_map(
535 entries: &[(Form, Form)],
536 scope: Option<&str>,
537 runtime: bool,
538) -> Vec<Dependency> {
539 entries
540 .iter()
541 .take(MAX_ITERATION_COUNT)
542 .filter_map(|(lib, coord)| build_deps_edn_dependency(lib, coord, scope, runtime))
543 .collect()
544}
545
546fn build_deps_edn_dependency(
547 lib: &Form,
548 coord: &Form,
549 scope: Option<&str>,
550 runtime: bool,
551) -> Option<Dependency> {
552 let (namespace, name) = parse_lib_form(lib)?;
553 let mut extra_data = HashMap::new();
554 let mut requirement = None;
555 let mut pinned = false;
556
557 if let Form::Map(entries) = coord {
558 if let Some(version) = map_get_keyword(entries, "mvn/version").and_then(form_as_string) {
559 requirement = Some(version.to_string());
560 pinned = is_exact_version(version);
561 }
562 for (key, data_key) in [
563 ("git/url", "git_url"),
564 ("git/tag", "git_tag"),
565 ("git/sha", "git_sha"),
566 ("deps/root", "deps_root"),
567 ("deps/manifest", "deps_manifest"),
568 ("local/root", "local_root"),
569 ("exclusions", "exclusions"),
570 ] {
571 if let Some(value) = map_get_keyword(entries, key)
572 .and_then(|f| form_to_json(f, &mut RecursionGuard::depth_only()))
573 {
574 extra_data.insert(data_key.to_string(), value);
575 }
576 }
577 }
578
579 Some(Dependency {
580 purl: build_maven_purl(
581 namespace.as_deref(),
582 &name,
583 requirement.as_deref().map(strip_exact_prefix),
584 )
585 .map(truncate_field),
586 extracted_requirement: requirement.map(truncate_field),
587 scope: scope.map(ToOwned::to_owned),
588 is_runtime: Some(runtime),
589 is_optional: Some(scope.is_some()),
590 is_pinned: Some(pinned),
591 is_direct: Some(true),
592 resolved_package: None,
593 extra_data: (!extra_data.is_empty()).then_some(extra_data),
594 })
595}
596
597fn extract_project_dependencies(entries: &[Form], scope: Option<&str>) -> Vec<Dependency> {
598 entries
599 .iter()
600 .take(MAX_ITERATION_COUNT)
601 .filter_map(|entry| {
602 let Form::Vector(parts) = entry else {
603 return None;
604 };
605 let (namespace, name) = parse_lib_form(parts.first()?)?;
606 let version = form_as_string(parts.get(1)?)?;
607
608 let mut extra_data = HashMap::new();
609 let mut index = 2usize;
610 while index + 1 < parts.len() {
611 if let Some(key) = form_as_keyword(&parts[index])
612 && let Some(value) =
613 form_to_json(&parts[index + 1], &mut RecursionGuard::depth_only())
614 {
615 extra_data.insert(key.replace('-', "_"), value);
616 }
617 index += 2;
618 }
619
620 let (is_runtime, is_optional) = match scope {
621 Some("dev") | Some("test") => (false, true),
622 Some("provided") => (false, false),
623 Some(_) => (false, true),
624 None => (true, false),
625 };
626
627 Some(Dependency {
628 purl: build_maven_purl(
629 namespace.as_deref(),
630 &name,
631 Some(strip_exact_prefix(version)),
632 )
633 .map(truncate_field),
634 extracted_requirement: Some(truncate_field(version.to_string())),
635 scope: scope.map(ToOwned::to_owned),
636 is_runtime: Some(is_runtime),
637 is_optional: Some(is_optional),
638 is_pinned: Some(is_exact_version(version)),
639 is_direct: Some(true),
640 resolved_package: None,
641 extra_data: (!extra_data.is_empty()).then_some(extra_data),
642 })
643 })
644 .collect()
645}
646
647fn parse_lib_form(form: &Form) -> Option<(Option<String>, String)> {
648 let raw = match form {
649 Form::Symbol(value) | Form::String(value) => value,
650 _ => return None,
651 };
652
653 if let Some((namespace, name)) = raw.split_once('/') {
654 Some((Some(namespace.to_string()), name.to_string()))
655 } else {
656 Some((Some(raw.to_string()), raw.to_string()))
657 }
658}
659
660fn map_get_keyword<'a>(entries: &'a [(Form, Form)], key: &str) -> Option<&'a Form> {
661 entries.iter().find_map(|(entry_key, entry_value)| {
662 if form_as_keyword(entry_key) == Some(key) {
663 Some(entry_value)
664 } else {
665 None
666 }
667 })
668}
669
670fn form_as_keyword(form: &Form) -> Option<&str> {
671 match form {
672 Form::Keyword(value) => Some(value.as_str()),
673 _ => None,
674 }
675}
676
677fn form_as_string(form: &Form) -> Option<&str> {
678 match form {
679 Form::String(value) => Some(value.as_str()),
680 _ => None,
681 }
682}
683
684fn keyword_or_symbol_name(form: &Form) -> Option<String> {
685 match form {
686 Form::Keyword(value) | Form::Symbol(value) => Some(value.clone()),
687 _ => None,
688 }
689}
690
691fn map_key_name(form: &Form) -> Option<String> {
692 match form {
693 Form::Keyword(value) | Form::Symbol(value) | Form::String(value) => Some(value.clone()),
694 _ => None,
695 }
696}
697
698fn form_to_json(form: &Form, guard: &mut RecursionGuard<()>) -> Option<JsonValue> {
699 if guard.descend() {
700 warn!("form_to_json exceeded MAX_RECURSION_DEPTH");
701 return None;
702 }
703 let result = Some(match form {
704 Form::Nil => JsonValue::Null,
705 Form::Bool(value) => JsonValue::Bool(*value),
706 Form::String(value) => JsonValue::String(value.clone()),
707 Form::Keyword(value) => JsonValue::String(format!(":{value}")),
708 Form::Symbol(value) => JsonValue::String(value.clone()),
709 Form::Vector(values) | Form::List(values) => JsonValue::Array(
710 values
711 .iter()
712 .filter_map(|f| form_to_json(f, guard))
713 .collect(),
714 ),
715 Form::Map(entries) => {
716 let mut map = serde_json::Map::new();
717 for (key, value) in entries {
718 let Some(key_name) = map_key_name(key) else {
719 continue;
720 };
721 if let Some(json) = form_to_json(value, guard) {
722 map.insert(key_name, json);
723 }
724 }
725 JsonValue::Object(map)
726 }
727 Form::Prefixed(value) => form_to_json(value, guard)?,
728 });
729 guard.ascend();
730 result
731}
732
733fn format_license(form: &Form, guard: &mut RecursionGuard<()>) -> Option<String> {
734 if guard.descend() {
735 warn!("format_license exceeded MAX_RECURSION_DEPTH");
736 return None;
737 }
738 let result = match form {
739 Form::Map(entries) => format_license_map(entries),
740 Form::Vector(values) | Form::List(values) => {
741 let licenses: Vec<String> = values
742 .iter()
743 .filter_map(|f| format_license(f, guard))
744 .collect();
745 if licenses.is_empty() {
746 None
747 } else {
748 Some(licenses.join("\n"))
749 }
750 }
751 _ => None,
752 };
753 guard.ascend();
754 result
755}
756
757fn format_license_map(entries: &[(Form, Form)]) -> Option<String> {
758 let name = map_get_keyword(entries, "name").and_then(form_as_string)?;
759 let mut rendered = format!("- license:\n name: {name}\n");
760 if let Some(url) = map_get_keyword(entries, "url").and_then(form_as_string) {
761 rendered.push_str(&format!(" url: {url}\n"));
762 }
763 Some(rendered)
764}
765
766fn build_maven_purl(namespace: Option<&str>, name: &str, version: Option<&str>) -> Option<String> {
767 let mut purl = PackageUrl::new(PackageType::Maven.as_str(), name).ok()?;
768 if let Some(namespace) = namespace {
769 purl.with_namespace(namespace).ok()?;
770 }
771 if let Some(version) = version {
772 purl.with_version(version).ok()?;
773 }
774 Some(purl.to_string())
775}
776
777fn is_exact_version(version: &str) -> bool {
778 let normalized = strip_exact_prefix(version).trim();
779 !normalized.is_empty()
780 && !normalized.contains('*')
781 && !normalized.contains('^')
782 && !normalized.contains('~')
783 && !normalized.contains('>')
784 && !normalized.contains('<')
785 && !normalized.contains('|')
786 && !normalized.contains(',')
787 && !normalized.contains(' ')
788}
789
790fn strip_exact_prefix(version: &str) -> &str {
791 version.trim_start_matches('=')
792}
793
794fn looks_like_template_project_clj(content: &str) -> bool {
795 let Some(defproject_index) = content.find("(defproject") else {
796 return false;
797 };
798
799 let manifest_window = &content[defproject_index..content.len().min(defproject_index + 256)];
800 manifest_window.contains("{{") && manifest_window.contains("}}")
801}
802
803fn default_package_data(datasource_id: Option<DatasourceId>) -> PackageData {
804 PackageData {
805 package_type: Some(PackageType::Maven),
806 primary_language: Some("Clojure".to_string()),
807 datasource_id,
808 ..Default::default()
809 }
810}
811
812crate::register_parser!(
813 "Clojure deps.edn and project.clj manifests",
814 &["**/deps.edn", "**/project.clj"],
815 "maven",
816 "Clojure",
817 Some("https://clojure.org/reference/deps_edn"),
818);