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