Skip to main content

provenant/parsers/
clojure.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::parser_warn as warn;
5use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
6use packageurl::PackageUrl;
7use serde_json::Value as JsonValue;
8
9use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
10
11use super::PackageParser;
12
13const MAX_RECURSION_DEPTH: usize = 50;
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    depth: usize,
121}
122
123impl Reader {
124    fn new(input: &str) -> Self {
125        Self {
126            chars: input.chars().collect(),
127            index: 0,
128            depth: 0,
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.depth > MAX_RECURSION_DEPTH {
169            return Err("recursion depth exceeded".to_string());
170        }
171        self.skip_ws_and_comments();
172        match self.peek() {
173            Some('"') => self.parse_string().map(Form::String),
174            Some(':') => self.parse_keyword().map(Form::Keyword),
175            Some('[') => {
176                self.depth += 1;
177                let result = self.parse_collection('[', ']').map(Form::Vector);
178                self.depth -= 1;
179                result
180            }
181            Some('(') => {
182                self.depth += 1;
183                let result = self.parse_collection('(', ')').map(Form::List);
184                self.depth -= 1;
185                result
186            }
187            Some('{') => {
188                self.depth += 1;
189                let result = self.parse_map();
190                self.depth -= 1;
191                result
192            }
193            Some('^') => {
194                self.index += 1;
195                self.depth += 1;
196                let _ = self.parse_form()?;
197                let result = self.parse_form();
198                self.depth -= 1;
199                result
200            }
201            Some('~') | Some('\'') | Some('`') | Some('@') => {
202                self.index += 1;
203                self.depth += 1;
204                let form = self.parse_form()?;
205                self.depth -= 1;
206                Ok(Form::Prefixed(Box::new(form)))
207            }
208            Some('#') => {
209                self.depth += 1;
210                let result = self.parse_dispatch_form();
211                self.depth -= 1;
212                result
213            }
214            Some(_) => self.parse_atom(),
215            None => Err("unexpected end of input".to_string()),
216        }
217    }
218
219    fn parse_dispatch_form(&mut self) -> Result<Form, String> {
220        self.expect('#')?;
221        match self.peek() {
222            Some('_') => {
223                self.index += 1;
224                let _ = self.parse_form()?;
225                self.parse_form()
226            }
227            Some('=') => Err("unsupported reader eval dispatch".to_string()),
228            Some('"') => {
229                // Tolerate regex literals in ignored fields without implementing reader semantics.
230                self.parse_string().map(Form::String)
231            }
232            Some('{') => {
233                // Tolerate set literals in ignored fields by treating them as plain collections.
234                self.parse_collection('{', '}').map(Form::Vector)
235            }
236            Some('(') => {
237                // Tolerate function literals in ignored fields without implementing reader semantics.
238                self.parse_collection('(', ')').map(Form::List)
239            }
240            Some('?') => {
241                // Tolerate reader conditionals by skipping the dispatch token and
242                // returning the selected readable form without evaluating features.
243                self.index += 1;
244                if self.peek() == Some('@') {
245                    self.index += 1;
246                }
247                let _ = self.parse_form()?;
248                self.parse_form()
249            }
250            Some(ch) if !is_delimiter(ch) => {
251                // Tolerate tagged literals in ignored fields by ignoring the tag and
252                // parsing the following readable form as plain data.
253                let _ = self.parse_atom()?;
254                self.parse_form()
255            }
256            Some(ch) => Err(format!("unsupported reader dispatch '#{ch}'")),
257            None => Err("unexpected end of input after '#'".to_string()),
258        }
259    }
260
261    fn parse_string(&mut self) -> Result<String, String> {
262        self.expect('"')?;
263        let mut result = String::new();
264        let mut escaped = false;
265        while let Some(ch) = self.peek() {
266            self.index += 1;
267            if escaped {
268                result.push(match ch {
269                    'n' => '\n',
270                    'r' => '\r',
271                    't' => '\t',
272                    '"' => '"',
273                    '\\' => '\\',
274                    other => other,
275                });
276                escaped = false;
277            } else if ch == '\\' {
278                escaped = true;
279            } else if ch == '"' {
280                return Ok(result);
281            } else {
282                result.push(ch);
283            }
284        }
285        Err("unterminated string".to_string())
286    }
287
288    fn parse_keyword(&mut self) -> Result<String, String> {
289        self.expect(':')?;
290        let start = self.index;
291        while let Some(ch) = self.peek() {
292            if is_delimiter(ch) {
293                break;
294            }
295            self.index += 1;
296        }
297        if self.index == start {
298            return Err("empty keyword".to_string());
299        }
300        Ok(self.chars[start..self.index].iter().collect())
301    }
302
303    fn parse_collection(&mut self, open: char, close: char) -> Result<Vec<Form>, String> {
304        self.expect(open)?;
305        let mut forms = Vec::new();
306        let mut count = 0usize;
307        loop {
308            self.skip_ws_and_comments();
309            if self.peek() == Some(close) {
310                self.index += 1;
311                return Ok(forms);
312            }
313            if self.peek().is_none() {
314                return Err(format!("unterminated collection starting with {open}"));
315            }
316            count += 1;
317            if count > MAX_ITERATION_COUNT {
318                warn!("Reached MAX_ITERATION_COUNT in parse_collection, stopping early");
319                break;
320            }
321            forms.push(self.parse_form()?);
322        }
323        Ok(forms)
324    }
325
326    fn parse_map(&mut self) -> Result<Form, String> {
327        self.expect('{')?;
328        let mut entries = Vec::new();
329        let mut count = 0usize;
330        loop {
331            self.skip_ws_and_comments();
332            if self.peek() == Some('}') {
333                self.index += 1;
334                return Ok(Form::Map(entries));
335            }
336            if self.peek().is_none() {
337                return Err("unterminated map".to_string());
338            }
339            count += 1;
340            if count > MAX_ITERATION_COUNT {
341                warn!("Reached MAX_ITERATION_COUNT in parse_map, stopping early");
342                break;
343            }
344            let key = self.parse_form()?;
345            self.skip_ws_and_comments();
346            if self.peek() == Some('}') {
347                return Err("map missing value".to_string());
348            }
349            let value = self.parse_form()?;
350            entries.push((key, value));
351        }
352        Ok(Form::Map(entries))
353    }
354
355    fn parse_atom(&mut self) -> Result<Form, String> {
356        let start = self.index;
357        while let Some(ch) = self.peek() {
358            if is_delimiter(ch) {
359                break;
360            }
361            self.index += 1;
362        }
363        let token: String = self.chars[start..self.index].iter().collect();
364        if token.is_empty() {
365            return Err("empty token".to_string());
366        }
367        Ok(match token.as_str() {
368            "nil" => Form::Nil,
369            "true" => Form::Bool(true),
370            "false" => Form::Bool(false),
371            _ => Form::Symbol(token),
372        })
373    }
374
375    fn expect(&mut self, expected: char) -> Result<(), String> {
376        match self.peek() {
377            Some(ch) if ch == expected => {
378                self.index += 1;
379                Ok(())
380            }
381            Some(ch) => Err(format!("expected '{expected}', found '{ch}'")),
382            None => Err(format!("expected '{expected}', found end of input")),
383        }
384    }
385
386    fn peek(&self) -> Option<char> {
387        self.chars.get(self.index).copied()
388    }
389}
390
391fn is_delimiter(ch: char) -> bool {
392    ch.is_whitespace()
393        || ch == ','
394        || matches!(
395            ch,
396            '[' | ']' | '{' | '}' | '(' | ')' | '"' | ';' | '\'' | '`' | '~' | '@'
397        )
398}
399
400fn parse_forms(input: &str) -> Result<Vec<Form>, String> {
401    Reader::new(input).parse_all()
402}
403
404fn parse_deps_edn_form(form: &Form) -> Result<PackageData, String> {
405    let Form::Map(entries) = form else {
406        return Err("deps.edn root is not a map".to_string());
407    };
408
409    let mut package = default_package_data(Some(DatasourceId::ClojureDepsEdn));
410    let mut dependencies = Vec::new();
411    let mut extra_data = HashMap::new();
412
413    if let Some(Form::Map(dep_map)) = map_get_keyword(entries, "deps") {
414        dependencies.extend(extract_deps_map(dep_map, None, true));
415    }
416
417    if let Some(Form::Map(alias_map)) = map_get_keyword(entries, "aliases") {
418        for (alias_key, alias_value) in alias_map {
419            let Some(alias_name) = keyword_or_symbol_name(alias_key) else {
420                continue;
421            };
422            let Form::Map(alias_entries) = alias_value else {
423                continue;
424            };
425            for dep_key in [
426                "extra-deps",
427                "override-deps",
428                "default-deps",
429                "deps",
430                "replace-deps",
431            ] {
432                if let Some(Form::Map(dep_map)) = map_get_keyword(alias_entries, dep_key) {
433                    dependencies.extend(extract_deps_map(dep_map, Some(&alias_name), false));
434                }
435            }
436        }
437        if let Some(json) = form_to_json(&Form::Map(alias_map.clone())) {
438            extra_data.insert("aliases".to_string(), json);
439        }
440    }
441
442    if let Some(value) = map_get_keyword(entries, "paths").and_then(form_to_json) {
443        extra_data.insert("paths".to_string(), value);
444    }
445    if let Some(value) = map_get_keyword(entries, "mvn/repos").and_then(form_to_json) {
446        extra_data.insert("mvn_repos".to_string(), value);
447    }
448
449    package.dependencies = dependencies;
450    package.extra_data = (!extra_data.is_empty()).then_some(extra_data);
451    Ok(package)
452}
453
454fn parse_project_clj_form(form: &Form) -> Result<PackageData, String> {
455    let Form::List(items) = form else {
456        return Err("project.clj root is not a list".to_string());
457    };
458    if !matches!(items.first(), Some(Form::Symbol(symbol)) if symbol == "defproject") {
459        return Err("project.clj root is not defproject".to_string());
460    }
461
462    let Some((namespace, name)) = items.get(1).and_then(parse_lib_form) else {
463        return Err("defproject missing project identifier".to_string());
464    };
465    let Some(version) = items.get(2).and_then(form_as_string) else {
466        return Err("defproject missing project version".to_string());
467    };
468
469    let mut package = default_package_data(Some(DatasourceId::ClojureProjectClj));
470    package.namespace = namespace.clone().map(truncate_field);
471    package.name = Some(truncate_field(name.clone()));
472    package.version = Some(truncate_field(version.to_string()));
473    package.purl = build_maven_purl(namespace.as_deref(), &name, Some(version)).map(truncate_field);
474
475    let mut index = 3usize;
476    while index + 1 < items.len() {
477        let Some(key) = form_as_keyword(&items[index]) else {
478            index += 1;
479            continue;
480        };
481        let value = &items[index + 1];
482
483        match key {
484            "description" => {
485                package.description = form_as_string(value).map(|s| truncate_field(s.to_owned()))
486            }
487            "url" => {
488                package.homepage_url = form_as_string(value).map(|s| truncate_field(s.to_owned()))
489            }
490            "license" => {
491                package.extracted_license_statement = format_license(value).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).and_then(form_to_json) {
572                extra_data.insert(data_key.to_string(), value);
573            }
574        }
575    }
576
577    Some(Dependency {
578        purl: build_maven_purl(
579            namespace.as_deref(),
580            &name,
581            requirement.as_deref().map(strip_exact_prefix),
582        )
583        .map(truncate_field),
584        extracted_requirement: requirement.map(truncate_field),
585        scope: scope.map(ToOwned::to_owned),
586        is_runtime: Some(runtime),
587        is_optional: Some(scope.is_some()),
588        is_pinned: Some(pinned),
589        is_direct: Some(true),
590        resolved_package: None,
591        extra_data: (!extra_data.is_empty()).then_some(extra_data),
592    })
593}
594
595fn extract_project_dependencies(entries: &[Form], scope: Option<&str>) -> Vec<Dependency> {
596    entries
597        .iter()
598        .take(MAX_ITERATION_COUNT)
599        .filter_map(|entry| {
600            let Form::Vector(parts) = entry else {
601                return None;
602            };
603            let (namespace, name) = parse_lib_form(parts.first()?)?;
604            let version = form_as_string(parts.get(1)?)?;
605
606            let mut extra_data = HashMap::new();
607            let mut index = 2usize;
608            while index + 1 < parts.len() {
609                if let Some(key) = form_as_keyword(&parts[index])
610                    && let Some(value) = form_to_json(&parts[index + 1])
611                {
612                    extra_data.insert(key.replace('-', "_"), value);
613                }
614                index += 2;
615            }
616
617            let (is_runtime, is_optional) = match scope {
618                Some("dev") | Some("test") => (false, true),
619                Some("provided") => (false, false),
620                Some(_) => (false, true),
621                None => (true, false),
622            };
623
624            Some(Dependency {
625                purl: build_maven_purl(
626                    namespace.as_deref(),
627                    &name,
628                    Some(strip_exact_prefix(version)),
629                )
630                .map(truncate_field),
631                extracted_requirement: Some(truncate_field(version.to_string())),
632                scope: scope.map(ToOwned::to_owned),
633                is_runtime: Some(is_runtime),
634                is_optional: Some(is_optional),
635                is_pinned: Some(is_exact_version(version)),
636                is_direct: Some(true),
637                resolved_package: None,
638                extra_data: (!extra_data.is_empty()).then_some(extra_data),
639            })
640        })
641        .collect()
642}
643
644fn parse_lib_form(form: &Form) -> Option<(Option<String>, String)> {
645    let raw = match form {
646        Form::Symbol(value) | Form::String(value) => value,
647        _ => return None,
648    };
649
650    if let Some((namespace, name)) = raw.split_once('/') {
651        Some((Some(namespace.to_string()), name.to_string()))
652    } else {
653        Some((Some(raw.to_string()), raw.to_string()))
654    }
655}
656
657fn map_get_keyword<'a>(entries: &'a [(Form, Form)], key: &str) -> Option<&'a Form> {
658    entries.iter().find_map(|(entry_key, entry_value)| {
659        if form_as_keyword(entry_key) == Some(key) {
660            Some(entry_value)
661        } else {
662            None
663        }
664    })
665}
666
667fn form_as_keyword(form: &Form) -> Option<&str> {
668    match form {
669        Form::Keyword(value) => Some(value.as_str()),
670        _ => None,
671    }
672}
673
674fn form_as_string(form: &Form) -> Option<&str> {
675    match form {
676        Form::String(value) => Some(value.as_str()),
677        _ => None,
678    }
679}
680
681fn keyword_or_symbol_name(form: &Form) -> Option<String> {
682    match form {
683        Form::Keyword(value) | Form::Symbol(value) => Some(value.clone()),
684        _ => None,
685    }
686}
687
688fn map_key_name(form: &Form) -> Option<String> {
689    match form {
690        Form::Keyword(value) | Form::Symbol(value) | Form::String(value) => Some(value.clone()),
691        _ => None,
692    }
693}
694
695fn form_to_json(form: &Form) -> Option<JsonValue> {
696    Some(match form {
697        Form::Nil => JsonValue::Null,
698        Form::Bool(value) => JsonValue::Bool(*value),
699        Form::String(value) => JsonValue::String(value.clone()),
700        Form::Keyword(value) => JsonValue::String(format!(":{value}")),
701        Form::Symbol(value) => JsonValue::String(value.clone()),
702        Form::Vector(values) | Form::List(values) => {
703            JsonValue::Array(values.iter().filter_map(form_to_json).collect())
704        }
705        Form::Map(entries) => {
706            let mut map = serde_json::Map::new();
707            for (key, value) in entries {
708                let Some(key_name) = map_key_name(key) else {
709                    continue;
710                };
711                if let Some(json) = form_to_json(value) {
712                    map.insert(key_name, json);
713                }
714            }
715            JsonValue::Object(map)
716        }
717        Form::Prefixed(value) => form_to_json(value)?,
718    })
719}
720
721fn format_license(form: &Form) -> Option<String> {
722    match form {
723        Form::Map(entries) => format_license_map(entries),
724        Form::Vector(values) | Form::List(values) => {
725            let licenses: Vec<String> = values.iter().filter_map(format_license).collect();
726            if licenses.is_empty() {
727                None
728            } else {
729                Some(licenses.join("\n"))
730            }
731        }
732        _ => None,
733    }
734}
735
736fn format_license_map(entries: &[(Form, Form)]) -> Option<String> {
737    let name = map_get_keyword(entries, "name").and_then(form_as_string)?;
738    let mut rendered = format!("- license:\n    name: {name}\n");
739    if let Some(url) = map_get_keyword(entries, "url").and_then(form_as_string) {
740        rendered.push_str(&format!("    url: {url}\n"));
741    }
742    Some(rendered)
743}
744
745fn build_maven_purl(namespace: Option<&str>, name: &str, version: Option<&str>) -> Option<String> {
746    let mut purl = PackageUrl::new(PackageType::Maven.as_str(), name).ok()?;
747    if let Some(namespace) = namespace {
748        purl.with_namespace(namespace).ok()?;
749    }
750    if let Some(version) = version {
751        purl.with_version(version).ok()?;
752    }
753    Some(purl.to_string())
754}
755
756fn is_exact_version(version: &str) -> bool {
757    let normalized = strip_exact_prefix(version).trim();
758    !normalized.is_empty()
759        && !normalized.contains('*')
760        && !normalized.contains('^')
761        && !normalized.contains('~')
762        && !normalized.contains('>')
763        && !normalized.contains('<')
764        && !normalized.contains('|')
765        && !normalized.contains(',')
766        && !normalized.contains(' ')
767}
768
769fn strip_exact_prefix(version: &str) -> &str {
770    version.trim_start_matches('=')
771}
772
773fn looks_like_template_project_clj(content: &str) -> bool {
774    let Some(defproject_index) = content.find("(defproject") else {
775        return false;
776    };
777
778    let manifest_window = &content[defproject_index..content.len().min(defproject_index + 256)];
779    manifest_window.contains("{{") && manifest_window.contains("}}")
780}
781
782fn default_package_data(datasource_id: Option<DatasourceId>) -> PackageData {
783    PackageData {
784        package_type: Some(PackageType::Maven),
785        primary_language: Some("Clojure".to_string()),
786        datasource_id,
787        ..Default::default()
788    }
789}
790
791crate::register_parser!(
792    "Clojure deps.edn and project.clj manifests",
793    &["**/deps.edn", "**/project.clj"],
794    "maven",
795    "Clojure",
796    Some("https://clojure.org/reference/deps_edn"),
797);