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.parse_string().map(Form::String)
218 }
219 Some('{') => {
220 self.parse_collection('{', '}').map(Form::Vector)
222 }
223 Some('(') => {
224 self.parse_collection('(', ')').map(Form::List)
226 }
227 Some('?') => {
228 self.index += 1;
231 if self.peek() == Some('@') {
232 self.index += 1;
233 }
234 let _ = self.parse_form()?;
235 self.parse_form()
236 }
237 Some(ch) if !is_delimiter(ch) => {
238 let _ = self.parse_atom()?;
241 self.parse_form()
242 }
243 Some(ch) => Err(format!("unsupported reader dispatch '#{ch}'")),
244 None => Err("unexpected end of input after '#'".to_string()),
245 }
246 }
247
248 fn parse_string(&mut self) -> Result<String, String> {
249 self.expect('"')?;
250 let mut result = String::new();
251 let mut escaped = false;
252 while let Some(ch) = self.peek() {
253 self.index += 1;
254 if escaped {
255 result.push(match ch {
256 'n' => '\n',
257 'r' => '\r',
258 't' => '\t',
259 '"' => '"',
260 '\\' => '\\',
261 other => other,
262 });
263 escaped = false;
264 } else if ch == '\\' {
265 escaped = true;
266 } else if ch == '"' {
267 return Ok(result);
268 } else {
269 result.push(ch);
270 }
271 }
272 Err("unterminated string".to_string())
273 }
274
275 fn parse_keyword(&mut self) -> Result<String, String> {
276 self.expect(':')?;
277 let start = self.index;
278 while let Some(ch) = self.peek() {
279 if is_delimiter(ch) {
280 break;
281 }
282 self.index += 1;
283 }
284 if self.index == start {
285 return Err("empty keyword".to_string());
286 }
287 Ok(self.chars[start..self.index].iter().collect())
288 }
289
290 fn parse_collection(&mut self, open: char, close: char) -> Result<Vec<Form>, String> {
291 self.expect(open)?;
292 let mut forms = Vec::new();
293 let mut count = 0usize;
294 loop {
295 self.skip_ws_and_comments();
296 if self.peek() == Some(close) {
297 self.index += 1;
298 return Ok(forms);
299 }
300 if self.peek().is_none() {
301 return Err(format!("unterminated collection starting with {open}"));
302 }
303 count += 1;
304 if count > MAX_ITERATION_COUNT {
305 warn!("Reached MAX_ITERATION_COUNT in parse_collection, stopping early");
306 break;
307 }
308 forms.push(self.parse_form()?);
309 }
310 Ok(forms)
311 }
312
313 fn parse_map(&mut self) -> Result<Form, String> {
314 self.expect('{')?;
315 let mut entries = Vec::new();
316 let mut count = 0usize;
317 loop {
318 self.skip_ws_and_comments();
319 if self.peek() == Some('}') {
320 self.index += 1;
321 return Ok(Form::Map(entries));
322 }
323 if self.peek().is_none() {
324 return Err("unterminated map".to_string());
325 }
326 count += 1;
327 if count > MAX_ITERATION_COUNT {
328 warn!("Reached MAX_ITERATION_COUNT in parse_map, stopping early");
329 break;
330 }
331 let key = self.parse_form()?;
332 self.skip_ws_and_comments();
333 if self.peek() == Some('}') {
334 return Err("map missing value".to_string());
335 }
336 let value = self.parse_form()?;
337 entries.push((key, value));
338 }
339 Ok(Form::Map(entries))
340 }
341
342 fn parse_atom(&mut self) -> Result<Form, String> {
343 let start = self.index;
344 while let Some(ch) = self.peek() {
345 if is_delimiter(ch) {
346 break;
347 }
348 self.index += 1;
349 }
350 let token: String = self.chars[start..self.index].iter().collect();
351 if token.is_empty() {
352 return Err("empty token".to_string());
353 }
354 Ok(match token.as_str() {
355 "nil" => Form::Nil,
356 "true" => Form::Bool(true),
357 "false" => Form::Bool(false),
358 _ => Form::Symbol(token),
359 })
360 }
361
362 fn expect(&mut self, expected: char) -> Result<(), String> {
363 match self.peek() {
364 Some(ch) if ch == expected => {
365 self.index += 1;
366 Ok(())
367 }
368 Some(ch) => Err(format!("expected '{expected}', found '{ch}'")),
369 None => Err(format!("expected '{expected}', found end of input")),
370 }
371 }
372
373 fn peek(&self) -> Option<char> {
374 self.chars.get(self.index).copied()
375 }
376}
377
378fn is_delimiter(ch: char) -> bool {
379 ch.is_whitespace()
380 || ch == ','
381 || matches!(
382 ch,
383 '[' | ']' | '{' | '}' | '(' | ')' | '"' | ';' | '\'' | '`' | '~' | '@'
384 )
385}
386
387fn parse_forms(input: &str) -> Result<Vec<Form>, String> {
388 Reader::new(input).parse_all()
389}
390
391fn parse_deps_edn_form(form: &Form) -> Result<PackageData, String> {
392 let Form::Map(entries) = form else {
393 return Err("deps.edn root is not a map".to_string());
394 };
395
396 let mut package = default_package_data(Some(DatasourceId::ClojureDepsEdn));
397 let mut dependencies = Vec::new();
398 let mut extra_data = HashMap::new();
399
400 if let Some(Form::Map(dep_map)) = map_get_keyword(entries, "deps") {
401 dependencies.extend(extract_deps_map(dep_map, None, true));
402 }
403
404 if let Some(Form::Map(alias_map)) = map_get_keyword(entries, "aliases") {
405 for (alias_key, alias_value) in alias_map {
406 let Some(alias_name) = keyword_or_symbol_name(alias_key) else {
407 continue;
408 };
409 let Form::Map(alias_entries) = alias_value else {
410 continue;
411 };
412 for dep_key in [
413 "extra-deps",
414 "override-deps",
415 "default-deps",
416 "deps",
417 "replace-deps",
418 ] {
419 if let Some(Form::Map(dep_map)) = map_get_keyword(alias_entries, dep_key) {
420 dependencies.extend(extract_deps_map(dep_map, Some(&alias_name), false));
421 }
422 }
423 }
424 if let Some(json) = form_to_json(
425 &Form::Map(alias_map.clone()),
426 &mut RecursionGuard::depth_only(),
427 ) {
428 extra_data.insert("aliases".to_string(), json);
429 }
430 }
431
432 if let Some(value) = map_get_keyword(entries, "paths")
433 .and_then(|f| form_to_json(f, &mut RecursionGuard::depth_only()))
434 {
435 extra_data.insert("paths".to_string(), value);
436 }
437 if let Some(value) = map_get_keyword(entries, "mvn/repos")
438 .and_then(|f| form_to_json(f, &mut RecursionGuard::depth_only()))
439 {
440 extra_data.insert("mvn_repos".to_string(), value);
441 }
442
443 package.dependencies = dependencies;
444 package.extra_data = (!extra_data.is_empty()).then_some(extra_data);
445 Ok(package)
446}
447
448fn parse_project_clj_form(form: &Form) -> Result<PackageData, String> {
449 let Form::List(items) = form else {
450 return Err("project.clj root is not a list".to_string());
451 };
452 if !matches!(items.first(), Some(Form::Symbol(symbol)) if symbol == "defproject") {
453 return Err("project.clj root is not defproject".to_string());
454 }
455
456 let Some((namespace, name)) = items.get(1).and_then(parse_lib_form) else {
457 return Err("defproject missing project identifier".to_string());
458 };
459 let Some(version) = items.get(2).and_then(form_as_string) else {
460 return Err("defproject missing project version".to_string());
461 };
462
463 let mut package = default_package_data(Some(DatasourceId::ClojureProjectClj));
464 package.namespace = namespace.clone().map(truncate_field);
465 package.name = Some(truncate_field(name.clone()));
466 package.version = Some(truncate_field(version.to_string()));
467 package.purl = build_maven_purl(namespace.as_deref(), &name, Some(version)).map(truncate_field);
468
469 let mut index = 3usize;
470 while index + 1 < items.len() {
471 let Some(key) = form_as_keyword(&items[index]) else {
472 index += 1;
473 continue;
474 };
475 let value = &items[index + 1];
476
477 match key {
478 "description" => {
479 package.description = form_as_string(value).map(|s| truncate_field(s.to_owned()))
480 }
481 "url" => {
482 package.homepage_url = form_as_string(value).map(|s| truncate_field(s.to_owned()))
483 }
484 "license" => {
485 package.extracted_license_statement =
486 format_license(value, &mut RecursionGuard::depth_only()).map(truncate_field);
487 }
488 "scm" => {
489 if let Form::Map(entries) = value {
490 package.vcs_url = map_get_keyword(entries, "url")
491 .and_then(form_as_string)
492 .map(|s| truncate_field(s.to_owned()));
493 }
494 }
495 "dependencies" => {
496 if let Form::Vector(deps) = value {
497 package
498 .dependencies
499 .extend(extract_project_dependencies(deps, None));
500 }
501 }
502 "profiles" => {
503 if let Form::Map(entries) = value {
504 for (profile_key, profile_value) in entries {
505 let Some(profile_name) = keyword_or_symbol_name(profile_key) else {
506 continue;
507 };
508 let Form::Map(profile_entries) = profile_value else {
509 continue;
510 };
511 if let Some(Form::Vector(deps)) =
512 map_get_keyword(profile_entries, "dependencies")
513 {
514 package
515 .dependencies
516 .extend(extract_project_dependencies(deps, Some(&profile_name)));
517 }
518 }
519 }
520 }
521 _ => {}
522 }
523 index += 2;
524 }
525
526 Ok(package)
527}
528
529fn extract_deps_map(
530 entries: &[(Form, Form)],
531 scope: Option<&str>,
532 runtime: bool,
533) -> Vec<Dependency> {
534 entries
535 .iter()
536 .take(MAX_ITERATION_COUNT)
537 .filter_map(|(lib, coord)| build_deps_edn_dependency(lib, coord, scope, runtime))
538 .collect()
539}
540
541fn build_deps_edn_dependency(
542 lib: &Form,
543 coord: &Form,
544 scope: Option<&str>,
545 runtime: bool,
546) -> Option<Dependency> {
547 let (namespace, name) = parse_lib_form(lib)?;
548 let mut extra_data = HashMap::new();
549 let mut requirement = None;
550 let mut pinned = false;
551
552 if let Form::Map(entries) = coord {
553 if let Some(version) = map_get_keyword(entries, "mvn/version").and_then(form_as_string) {
554 requirement = Some(version.to_string());
555 pinned = is_exact_version(version);
556 }
557 for (key, data_key) in [
558 ("git/url", "git_url"),
559 ("git/tag", "git_tag"),
560 ("git/sha", "git_sha"),
561 ("deps/root", "deps_root"),
562 ("deps/manifest", "deps_manifest"),
563 ("local/root", "local_root"),
564 ("exclusions", "exclusions"),
565 ] {
566 if let Some(value) = map_get_keyword(entries, key)
567 .and_then(|f| form_to_json(f, &mut RecursionGuard::depth_only()))
568 {
569 extra_data.insert(data_key.to_string(), value);
570 }
571 }
572 }
573
574 Some(Dependency {
575 purl: build_maven_purl(
576 namespace.as_deref(),
577 &name,
578 requirement.as_deref().map(strip_exact_prefix),
579 )
580 .map(truncate_field),
581 extracted_requirement: requirement.map(truncate_field),
582 scope: scope.map(ToOwned::to_owned),
583 is_runtime: Some(runtime),
584 is_optional: Some(scope.is_some()),
585 is_pinned: Some(pinned),
586 is_direct: Some(true),
587 resolved_package: None,
588 extra_data: (!extra_data.is_empty()).then_some(extra_data),
589 })
590}
591
592fn extract_project_dependencies(entries: &[Form], scope: Option<&str>) -> Vec<Dependency> {
593 entries
594 .iter()
595 .take(MAX_ITERATION_COUNT)
596 .filter_map(|entry| {
597 let Form::Vector(parts) = entry else {
598 return None;
599 };
600 let (namespace, name) = parse_lib_form(parts.first()?)?;
601 let version = form_as_string(parts.get(1)?)?;
602
603 let mut extra_data = HashMap::new();
604 let mut index = 2usize;
605 while index + 1 < parts.len() {
606 if let Some(key) = form_as_keyword(&parts[index])
607 && let Some(value) =
608 form_to_json(&parts[index + 1], &mut RecursionGuard::depth_only())
609 {
610 extra_data.insert(key.replace('-', "_"), value);
611 }
612 index += 2;
613 }
614
615 let (is_runtime, is_optional) = match scope {
616 Some("dev") | Some("test") => (false, true),
617 Some("provided") => (false, false),
618 Some(_) => (false, true),
619 None => (true, false),
620 };
621
622 Some(Dependency {
623 purl: build_maven_purl(
624 namespace.as_deref(),
625 &name,
626 Some(strip_exact_prefix(version)),
627 )
628 .map(truncate_field),
629 extracted_requirement: Some(truncate_field(version.to_string())),
630 scope: scope.map(ToOwned::to_owned),
631 is_runtime: Some(is_runtime),
632 is_optional: Some(is_optional),
633 is_pinned: Some(is_exact_version(version)),
634 is_direct: Some(true),
635 resolved_package: None,
636 extra_data: (!extra_data.is_empty()).then_some(extra_data),
637 })
638 })
639 .collect()
640}
641
642fn parse_lib_form(form: &Form) -> Option<(Option<String>, String)> {
643 let raw = match form {
644 Form::Symbol(value) | Form::String(value) => value,
645 _ => return None,
646 };
647
648 if let Some((namespace, name)) = raw.split_once('/') {
649 Some((Some(namespace.to_string()), name.to_string()))
650 } else {
651 Some((Some(raw.to_string()), raw.to_string()))
652 }
653}
654
655fn map_get_keyword<'a>(entries: &'a [(Form, Form)], key: &str) -> Option<&'a Form> {
656 entries.iter().find_map(|(entry_key, entry_value)| {
657 if form_as_keyword(entry_key) == Some(key) {
658 Some(entry_value)
659 } else {
660 None
661 }
662 })
663}
664
665fn form_as_keyword(form: &Form) -> Option<&str> {
666 match form {
667 Form::Keyword(value) => Some(value.as_str()),
668 _ => None,
669 }
670}
671
672fn form_as_string(form: &Form) -> Option<&str> {
673 match form {
674 Form::String(value) => Some(value.as_str()),
675 _ => None,
676 }
677}
678
679fn keyword_or_symbol_name(form: &Form) -> Option<String> {
680 match form {
681 Form::Keyword(value) | Form::Symbol(value) => Some(value.clone()),
682 _ => None,
683 }
684}
685
686fn map_key_name(form: &Form) -> Option<String> {
687 match form {
688 Form::Keyword(value) | Form::Symbol(value) | Form::String(value) => Some(value.clone()),
689 _ => None,
690 }
691}
692
693fn form_to_json(form: &Form, guard: &mut RecursionGuard<()>) -> Option<JsonValue> {
694 if guard.descend() {
695 warn!("form_to_json exceeded MAX_RECURSION_DEPTH");
696 return None;
697 }
698 let result = Some(match form {
699 Form::Nil => JsonValue::Null,
700 Form::Bool(value) => JsonValue::Bool(*value),
701 Form::String(value) => JsonValue::String(value.clone()),
702 Form::Keyword(value) => JsonValue::String(format!(":{value}")),
703 Form::Symbol(value) => JsonValue::String(value.clone()),
704 Form::Vector(values) | Form::List(values) => JsonValue::Array(
705 values
706 .iter()
707 .filter_map(|f| form_to_json(f, guard))
708 .collect(),
709 ),
710 Form::Map(entries) => {
711 let mut map = serde_json::Map::new();
712 for (key, value) in entries {
713 let Some(key_name) = map_key_name(key) else {
714 continue;
715 };
716 if let Some(json) = form_to_json(value, guard) {
717 map.insert(key_name, json);
718 }
719 }
720 JsonValue::Object(map)
721 }
722 Form::Prefixed(value) => form_to_json(value, guard)?,
723 });
724 guard.ascend();
725 result
726}
727
728fn format_license(form: &Form, guard: &mut RecursionGuard<()>) -> Option<String> {
729 if guard.descend() {
730 warn!("format_license exceeded MAX_RECURSION_DEPTH");
731 return None;
732 }
733 let result = match form {
734 Form::Map(entries) => format_license_map(entries),
735 Form::Vector(values) | Form::List(values) => {
736 let licenses: Vec<String> = values
737 .iter()
738 .filter_map(|f| format_license(f, guard))
739 .collect();
740 if licenses.is_empty() {
741 None
742 } else {
743 Some(licenses.join("\n"))
744 }
745 }
746 _ => None,
747 };
748 guard.ascend();
749 result
750}
751
752fn format_license_map(entries: &[(Form, Form)]) -> Option<String> {
753 let name = map_get_keyword(entries, "name").and_then(form_as_string)?;
754 let mut rendered = format!("- license:\n name: {name}\n");
755 if let Some(url) = map_get_keyword(entries, "url").and_then(form_as_string) {
756 rendered.push_str(&format!(" url: {url}\n"));
757 }
758 Some(rendered)
759}
760
761fn build_maven_purl(namespace: Option<&str>, name: &str, version: Option<&str>) -> Option<String> {
762 let mut purl = PackageUrl::new(PackageType::Maven.as_str(), name).ok()?;
763 if let Some(namespace) = namespace {
764 purl.with_namespace(namespace).ok()?;
765 }
766 if let Some(version) = version {
767 purl.with_version(version).ok()?;
768 }
769 Some(purl.to_string())
770}
771
772fn is_exact_version(version: &str) -> bool {
773 let normalized = strip_exact_prefix(version).trim();
774 !normalized.is_empty()
775 && !normalized.contains('*')
776 && !normalized.contains('^')
777 && !normalized.contains('~')
778 && !normalized.contains('>')
779 && !normalized.contains('<')
780 && !normalized.contains('|')
781 && !normalized.contains(',')
782 && !normalized.contains(' ')
783}
784
785fn strip_exact_prefix(version: &str) -> &str {
786 version.trim_start_matches('=')
787}
788
789fn looks_like_template_project_clj(content: &str) -> bool {
790 let Some(defproject_index) = content.find("(defproject") else {
791 return false;
792 };
793
794 let manifest_window = &content[defproject_index..content.len().min(defproject_index + 256)];
795 manifest_window.contains("{{") && manifest_window.contains("}}")
796}
797
798fn default_package_data(datasource_id: Option<DatasourceId>) -> PackageData {
799 PackageData {
800 package_type: Some(PackageType::Maven),
801 primary_language: Some("Clojure".to_string()),
802 datasource_id,
803 ..Default::default()
804 }
805}
806
807crate::register_parser!(
808 "Clojure deps.edn and project.clj manifests",
809 &["**/deps.edn", "**/project.clj"],
810 "maven",
811 "Clojure",
812 Some("https://clojure.org/reference/deps_edn"),
813);