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