Skip to main content

cabalist_parser/
ast.rs

1//! Abstract Syntax Tree (AST) for `.cabal` files.
2//!
3//! The AST is a typed, ergonomic view derived from the CST. It provides
4//! structured access to package metadata, components, dependencies, and
5//! conditionals. Every AST node carries a [`NodeId`] back-reference to the
6//! corresponding CST node so that edits can be mapped back to the concrete tree.
7//!
8//! The AST does not own the source text: it borrows from the [`CabalCst`]'s
9//! source string.
10//!
11//! # Usage
12//!
13//! ```
14//! use cabalist_parser::{parse, ast::derive_ast};
15//!
16//! let source = "cabal-version: 3.0\nname: my-pkg\nversion: 0.1.0.0\n";
17//! let result = parse(source);
18//! let ast = derive_ast(&result.cst);
19//! assert_eq!(ast.name, Some("my-pkg"));
20//! ```
21
22use crate::cst::{CabalCst, CstNodeKind};
23use crate::span::NodeId;
24
25// ---------------------------------------------------------------------------
26// Field name canonicalization
27// ---------------------------------------------------------------------------
28
29/// Canonicalize a `.cabal` field name to lowercase with hyphens.
30///
31/// Field names in `.cabal` files are case-insensitive and treat hyphens and
32/// underscores as interchangeable. This function normalizes to the canonical
33/// `lowercase-with-hyphens` form.
34pub fn canonicalize_field_name(name: &str) -> String {
35    name.to_ascii_lowercase().replace('_', "-")
36}
37
38// ---------------------------------------------------------------------------
39// Version
40// ---------------------------------------------------------------------------
41
42/// A parsed version number like `0.1.0.0`.
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct Version {
45    pub components: Vec<u64>,
46}
47
48impl Version {
49    /// Parse a version string such as `"0.1.0.0"` or `"4.14"`.
50    ///
51    /// Returns `None` if the string is empty or contains non-numeric
52    /// components.
53    pub fn parse(s: &str) -> Option<Self> {
54        let s = s.trim();
55        if s.is_empty() {
56            return None;
57        }
58        let mut components = Vec::new();
59        for part in s.split('.') {
60            let part = part.trim();
61            if part.is_empty() {
62                return None;
63            }
64            match part.parse::<u64>() {
65                Ok(n) => components.push(n),
66                Err(_) => return None,
67            }
68        }
69        if components.is_empty() {
70            return None;
71        }
72        Some(Version { components })
73    }
74}
75
76impl std::fmt::Display for Version {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        let mut first = true;
79        for c in &self.components {
80            if !first {
81                write!(f, ".")?;
82            }
83            write!(f, "{c}")?;
84            first = false;
85        }
86        Ok(())
87    }
88}
89
90// ---------------------------------------------------------------------------
91// Version ranges
92// ---------------------------------------------------------------------------
93
94/// A version constraint expression.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum VersionRange {
97    /// No constraint: any version.
98    Any,
99    /// No version matches.
100    NoVersion,
101    /// `==V`
102    Eq(Version),
103    /// `>V`
104    Gt(Version),
105    /// `>=V`
106    Gte(Version),
107    /// `<V`
108    Lt(Version),
109    /// `<=V`
110    Lte(Version),
111    /// `^>=V` (PVP major bound)
112    MajorBound(Version),
113    /// Intersection: `A && B`
114    And(Box<VersionRange>, Box<VersionRange>),
115    /// Union: `A || B`
116    Or(Box<VersionRange>, Box<VersionRange>),
117}
118
119impl std::fmt::Display for VersionRange {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            VersionRange::Any => write!(f, "-any"),
123            VersionRange::NoVersion => write!(f, "-none"),
124            VersionRange::Eq(v) => write!(f, "=={v}"),
125            VersionRange::Gt(v) => write!(f, ">{v}"),
126            VersionRange::Gte(v) => write!(f, ">={v}"),
127            VersionRange::Lt(v) => write!(f, "<{v}"),
128            VersionRange::Lte(v) => write!(f, "<={v}"),
129            VersionRange::MajorBound(v) => write!(f, "^>={v}"),
130            VersionRange::And(a, b) => write!(f, "{a} && {b}"),
131            VersionRange::Or(a, b) => write!(f, "{a} || {b}"),
132        }
133    }
134}
135
136impl VersionRange {
137    /// Check if a version satisfies this version range.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use cabalist_parser::ast::{Version, VersionRange};
143    ///
144    /// let v = Version { components: vec![4, 17, 0, 0] };
145    /// let range = VersionRange::MajorBound(Version { components: vec![4, 17] });
146    /// assert!(range.satisfies(&v));
147    ///
148    /// let too_new = Version { components: vec![4, 18, 0, 0] };
149    /// assert!(!range.satisfies(&too_new));
150    /// ```
151    pub fn satisfies(&self, version: &Version) -> bool {
152        version_satisfies(version, self)
153    }
154}
155
156/// Check if a version satisfies a version range (best-effort).
157///
158/// This implements PVP-aware version comparison for all constraint operators.
159pub fn version_satisfies(version: &Version, vr: &VersionRange) -> bool {
160    use std::cmp::Ordering;
161
162    let cmp_versions = |a: &Version, b: &Version| -> Ordering {
163        let max_len = a.components.len().max(b.components.len());
164        for i in 0..max_len {
165            let ac = a.components.get(i).copied().unwrap_or(0);
166            let bc = b.components.get(i).copied().unwrap_or(0);
167            match ac.cmp(&bc) {
168                Ordering::Equal => continue,
169                other => return other,
170            }
171        }
172        Ordering::Equal
173    };
174
175    match vr {
176        VersionRange::Any => true,
177        VersionRange::NoVersion => false,
178        VersionRange::Eq(v) => cmp_versions(version, v) == Ordering::Equal,
179        VersionRange::Gt(v) => cmp_versions(version, v) == Ordering::Greater,
180        VersionRange::Gte(v) => cmp_versions(version, v) != Ordering::Less,
181        VersionRange::Lt(v) => cmp_versions(version, v) == Ordering::Less,
182        VersionRange::Lte(v) => cmp_versions(version, v) != Ordering::Greater,
183        VersionRange::MajorBound(v) => {
184            // ^>=X.Y means >=X.Y && <X.(Y+1)
185            if cmp_versions(version, v) == Ordering::Less {
186                return false;
187            }
188            let mut upper = v.clone();
189            if upper.components.len() >= 2 {
190                upper.components[1] += 1;
191                upper.components.truncate(2);
192            } else if upper.components.len() == 1 {
193                upper.components[0] += 1;
194            }
195            cmp_versions(version, &upper) == Ordering::Less
196        }
197        VersionRange::And(a, b) => version_satisfies(version, a) && version_satisfies(version, b),
198        VersionRange::Or(a, b) => version_satisfies(version, a) || version_satisfies(version, b),
199    }
200}
201
202/// Parse a version range string.
203///
204/// Handles expressions like:
205/// - `">=4.14 && <5"`
206/// - `"^>=2.2"`
207/// - `">=2.0 || ==1.9"`
208/// - `">=4.14"`
209/// - `"==1.0"`
210///
211/// Returns `None` if the string is empty or cannot be parsed.
212pub fn parse_version_range(s: &str) -> Option<VersionRange> {
213    let s = s.trim();
214    if s.is_empty() {
215        return None;
216    }
217
218    // Split on `||` first (lowest precedence), then `&&`.
219    if let Some(range) = parse_or_range(s) {
220        return Some(range);
221    }
222
223    None
224}
225
226/// Parse an `||`-separated version range (lowest precedence).
227fn parse_or_range(s: &str) -> Option<VersionRange> {
228    // Split on `||` respecting parentheses.
229    let parts = split_respecting_parens(s, "||");
230    if parts.len() > 1 {
231        let mut ranges: Vec<VersionRange> = Vec::new();
232        for part in &parts {
233            ranges.push(parse_and_range(part.trim())?);
234        }
235        let mut result = ranges.remove(0);
236        for r in ranges {
237            result = VersionRange::Or(Box::new(result), Box::new(r));
238        }
239        return Some(result);
240    }
241    parse_and_range(s)
242}
243
244/// Parse an `&&`-separated version range.
245fn parse_and_range(s: &str) -> Option<VersionRange> {
246    let parts = split_respecting_parens(s, "&&");
247    if parts.len() > 1 {
248        let mut ranges: Vec<VersionRange> = Vec::new();
249        for part in &parts {
250            ranges.push(parse_atom_range(part.trim())?);
251        }
252        let mut result = ranges.remove(0);
253        for r in ranges {
254            result = VersionRange::And(Box::new(result), Box::new(r));
255        }
256        return Some(result);
257    }
258    parse_atom_range(s)
259}
260
261/// Parse a single atomic version range: `>=V`, `<V`, `^>=V`, `==V`, etc.
262/// Also handles parenthesized sub-expressions.
263fn parse_atom_range(s: &str) -> Option<VersionRange> {
264    let s = s.trim();
265    if s.is_empty() {
266        return None;
267    }
268
269    // `-any` and `-none` keywords.
270    if s.eq_ignore_ascii_case("-any") {
271        return Some(VersionRange::Any);
272    }
273    if s.eq_ignore_ascii_case("-none") {
274        return Some(VersionRange::NoVersion);
275    }
276
277    // Parenthesized expression.
278    if s.starts_with('(') && s.ends_with(')') {
279        return parse_or_range(&s[1..s.len() - 1]);
280    }
281
282    // `^>=V` or `^>= { v1, v2, v3 }` (set notation)
283    if let Some(rest) = s.strip_prefix("^>=") {
284        let rest = rest.trim();
285        if rest.starts_with('{') && rest.ends_with('}') {
286            let inner = &rest[1..rest.len() - 1];
287            let versions: Vec<&str> = inner.split(',').map(|v| v.trim()).collect();
288            let mut ranges: Vec<VersionRange> = Vec::new();
289            for v_str in versions {
290                if !v_str.is_empty() {
291                    if let Some(v) = Version::parse(v_str) {
292                        ranges.push(VersionRange::MajorBound(v));
293                    }
294                }
295            }
296            if ranges.is_empty() {
297                return None;
298            }
299            let mut result = ranges.remove(0);
300            for r in ranges {
301                result = VersionRange::Or(Box::new(result), Box::new(r));
302            }
303            return Some(result);
304        }
305        let v = Version::parse(rest)?;
306        return Some(VersionRange::MajorBound(v));
307    }
308
309    // `>=V`
310    if let Some(rest) = s.strip_prefix(">=") {
311        let v = Version::parse(rest.trim())?;
312        return Some(VersionRange::Gte(v));
313    }
314
315    // `<=V`
316    if let Some(rest) = s.strip_prefix("<=") {
317        let v = Version::parse(rest.trim())?;
318        return Some(VersionRange::Lte(v));
319    }
320
321    // `==V`, `==V.*`, or `== { v1, v2 }` (set notation)
322    if let Some(rest) = s.strip_prefix("==") {
323        let rest = rest.trim();
324        // Set notation: == { v1, v2, v3 }
325        if rest.starts_with('{') && rest.ends_with('}') {
326            let inner = &rest[1..rest.len() - 1];
327            let versions: Vec<&str> = inner.split(',').map(|v| v.trim()).collect();
328            let mut ranges: Vec<VersionRange> = Vec::new();
329            for v_str in versions {
330                if !v_str.is_empty() {
331                    if let Some(v) = Version::parse(v_str) {
332                        ranges.push(VersionRange::Eq(v));
333                    }
334                }
335            }
336            if ranges.is_empty() {
337                return None;
338            }
339            let mut result = ranges.remove(0);
340            for r in ranges {
341                result = VersionRange::Or(Box::new(result), Box::new(r));
342            }
343            return Some(result);
344        }
345        // Wildcard: ==1.2.* means >= 1.2 && < 1.3
346        if let Some(prefix) = rest.strip_suffix(".*") {
347            let v = Version::parse(prefix)?;
348            let mut upper = v.clone();
349            if let Some(last) = upper.components.last_mut() {
350                *last += 1;
351            }
352            return Some(VersionRange::And(
353                Box::new(VersionRange::Gte(v)),
354                Box::new(VersionRange::Lt(upper)),
355            ));
356        }
357        let v = Version::parse(rest)?;
358        return Some(VersionRange::Eq(v));
359    }
360
361    // `>V` (must come after `>=`)
362    if let Some(rest) = s.strip_prefix('>') {
363        let v = Version::parse(rest.trim())?;
364        return Some(VersionRange::Gt(v));
365    }
366
367    // `<V` (must come after `<=`)
368    if let Some(rest) = s.strip_prefix('<') {
369        let v = Version::parse(rest.trim())?;
370        return Some(VersionRange::Lt(v));
371    }
372
373    None
374}
375
376/// Split a string on a delimiter, but not inside parentheses.
377fn split_respecting_parens<'a>(s: &'a str, delim: &str) -> Vec<&'a str> {
378    let mut parts = Vec::new();
379    let mut depth = 0usize;
380    let mut last = 0;
381    let bytes = s.as_bytes();
382    let delim_bytes = delim.as_bytes();
383    let delim_len = delim_bytes.len();
384
385    let mut i = 0;
386    while i < bytes.len() {
387        if bytes[i] == b'(' {
388            depth += 1;
389            i += 1;
390        } else if bytes[i] == b')' {
391            depth = depth.saturating_sub(1);
392            i += 1;
393        } else if depth == 0
394            && i + delim_len <= bytes.len()
395            && &bytes[i..i + delim_len] == delim_bytes
396        {
397            parts.push(&s[last..i]);
398            i += delim_len;
399            last = i;
400        } else {
401            i += 1;
402        }
403    }
404    parts.push(&s[last..]);
405    parts
406}
407
408// ---------------------------------------------------------------------------
409// Dependency
410// ---------------------------------------------------------------------------
411
412/// A parsed dependency from `build-depends`.
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct Dependency<'a> {
415    /// The package name (e.g. `"aeson"`, `"base"`).
416    pub package: &'a str,
417    /// The version constraint, if any.
418    pub version_range: Option<VersionRange>,
419    /// Back-reference to the CST node (the Field or ValueLine containing this
420    /// dependency).
421    pub cst_node: NodeId,
422}
423
424/// Parse a single dependency string like `"aeson ^>=2.2"` or `"base >=4.14 && <5"`.
425///
426/// The `cst_node` is attached to the resulting `Dependency` for back-reference.
427fn parse_single_dependency<'a>(s: &'a str, cst_node: NodeId) -> Option<Dependency<'a>> {
428    let s = s.trim();
429    if s.is_empty() {
430        return None;
431    }
432
433    // The package name is the first token (letters, digits, hyphens).
434    let name_end = s
435        .find(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
436        .unwrap_or(s.len());
437    let package = s[..name_end].trim();
438    if package.is_empty() {
439        return None;
440    }
441
442    let rest = s[name_end..].trim();
443    let version_range = if rest.is_empty() {
444        None
445    } else {
446        parse_version_range(rest)
447    };
448
449    Some(Dependency {
450        package,
451        version_range,
452        cst_node,
453    })
454}
455
456/// Parse a dependency field value (possibly containing multiple comma-separated
457/// dependencies) into a vector of [`Dependency`] values.
458///
459/// Handles:
460/// - Single line: `"base >=4.14, text >=2.0, aeson ^>=2.2"`
461/// - Individual items from multi-line fields (call once per line).
462fn parse_dependencies_from_text<'a>(text: &'a str, cst_node: NodeId) -> Vec<Dependency<'a>> {
463    let mut deps = Vec::new();
464    // Split on commas.
465    for part in text.split(',') {
466        let part = part.trim();
467        if part.is_empty() {
468            continue;
469        }
470        if let Some(dep) = parse_single_dependency(part, cst_node) {
471            deps.push(dep);
472        }
473    }
474    deps
475}
476
477// ---------------------------------------------------------------------------
478// Cabal version
479// ---------------------------------------------------------------------------
480
481/// The `cabal-version` specification.
482#[derive(Debug, Clone, PartialEq, Eq)]
483pub struct CabalVersion<'a> {
484    /// The raw text as written in the file.
485    pub raw: &'a str,
486    /// The parsed version, if it could be parsed.
487    pub version: Option<Version>,
488    /// Back-reference to the CST field node.
489    pub cst_node: NodeId,
490}
491
492// ---------------------------------------------------------------------------
493// Generic field
494// ---------------------------------------------------------------------------
495
496/// A field that was not specifically parsed into a typed representation.
497#[derive(Debug, Clone, PartialEq, Eq)]
498pub struct Field<'a> {
499    /// The canonicalized field name.
500    pub name: String,
501    /// The raw field name as written in the source.
502    pub raw_name: &'a str,
503    /// The field value (first line only; continuation lines are concatenated
504    /// with newlines).
505    pub value: String,
506    /// Back-reference to the CST field node.
507    pub cst_node: NodeId,
508}
509
510// ---------------------------------------------------------------------------
511// Condition AST
512// ---------------------------------------------------------------------------
513
514/// A parsed condition expression from `if`/`elif` blocks.
515#[derive(Debug, Clone, PartialEq, Eq)]
516pub enum Condition<'a> {
517    /// `flag(name)`
518    Flag(&'a str),
519    /// `os(name)`
520    OS(&'a str),
521    /// `arch(name)`
522    Arch(&'a str),
523    /// `impl(compiler version-range)`
524    Impl(&'a str, Option<VersionRange>),
525    /// `!condition`
526    Not(Box<Condition<'a>>),
527    /// `a && b`
528    And(Box<Condition<'a>>, Box<Condition<'a>>),
529    /// `a || b`
530    Or(Box<Condition<'a>>, Box<Condition<'a>>),
531    /// Boolean literal: `true` or `false`
532    Lit(bool),
533    /// Unparsed fallback when the condition couldn't be fully parsed.
534    Raw(&'a str),
535}
536
537/// Parse a condition expression string.
538///
539/// Handles expressions like:
540/// - `flag(dev)`
541/// - `os(windows)`
542/// - `flag(dev) && !os(windows)`
543/// - `impl(ghc >= 9.6)`
544/// - `(flag(a) || flag(b)) && !os(windows)`
545pub fn parse_condition(s: &str) -> Condition<'_> {
546    let s = s.trim();
547    if s.is_empty() {
548        return Condition::Raw(s);
549    }
550    match parse_condition_or(s) {
551        Some(c) => c,
552        None => Condition::Raw(s),
553    }
554}
555
556/// Parse `||` (lowest precedence).
557fn parse_condition_or(s: &str) -> Option<Condition<'_>> {
558    let parts = split_respecting_parens(s, "||");
559    if parts.len() > 1 {
560        let mut conds: Vec<Condition<'_>> = Vec::new();
561        for part in &parts {
562            conds.push(parse_condition_and(part.trim())?);
563        }
564        let mut result = conds.remove(0);
565        for c in conds {
566            result = Condition::Or(Box::new(result), Box::new(c));
567        }
568        return Some(result);
569    }
570    parse_condition_and(s)
571}
572
573/// Parse `&&`.
574fn parse_condition_and(s: &str) -> Option<Condition<'_>> {
575    let parts = split_respecting_parens(s, "&&");
576    if parts.len() > 1 {
577        let mut conds: Vec<Condition<'_>> = Vec::new();
578        for part in &parts {
579            conds.push(parse_condition_atom(part.trim())?);
580        }
581        let mut result = conds.remove(0);
582        for c in conds {
583            result = Condition::And(Box::new(result), Box::new(c));
584        }
585        return Some(result);
586    }
587    parse_condition_atom(s)
588}
589
590/// Parse a single condition atom: `!cond`, `flag(name)`, `os(name)`,
591/// `arch(name)`, `impl(...)`, or parenthesized sub-expression.
592fn parse_condition_atom(s: &str) -> Option<Condition<'_>> {
593    let s = s.trim();
594    if s.is_empty() {
595        return None;
596    }
597
598    // Negation.
599    if let Some(rest) = s.strip_prefix('!') {
600        let inner = parse_condition_atom(rest.trim())?;
601        return Some(Condition::Not(Box::new(inner)));
602    }
603
604    // Parenthesized.
605    if s.starts_with('(') && s.ends_with(')') {
606        return parse_condition_or(&s[1..s.len() - 1]);
607    }
608
609    // `flag(name)`, `os(name)`, `arch(name)`, `impl(...)`.
610    if let Some(paren_start) = s.find('(') {
611        if s.ends_with(')') {
612            let func = s[..paren_start].trim();
613            let arg = s[paren_start + 1..s.len() - 1].trim();
614            let func_lower = func.to_ascii_lowercase();
615            match func_lower.as_str() {
616                "flag" => return Some(Condition::Flag(arg)),
617                "os" => return Some(Condition::OS(arg)),
618                "arch" => return Some(Condition::Arch(arg)),
619                "impl" => {
620                    // arg could be e.g. "ghc >= 9.6" or just "ghc".
621                    let parts: Vec<&str> = arg.splitn(2, char::is_whitespace).collect();
622                    let compiler = parts[0];
623                    let vr = if parts.len() > 1 {
624                        parse_version_range(parts[1].trim())
625                    } else {
626                        None
627                    };
628                    return Some(Condition::Impl(compiler, vr));
629                }
630                _ => {}
631            }
632        }
633    }
634
635    // Boolean literals
636    match s.to_ascii_lowercase().as_str() {
637        "true" => return Some(Condition::Lit(true)),
638        "false" => return Some(Condition::Lit(false)),
639        _ => {}
640    }
641
642    // Could not parse: return Raw for the whole string.
643    Some(Condition::Raw(s))
644}
645
646// ---------------------------------------------------------------------------
647// Conditional block
648// ---------------------------------------------------------------------------
649
650/// A conditional block (`if`/`elif` with optional `else`) inside a component.
651#[derive(Debug, Clone, PartialEq, Eq)]
652pub struct Conditional<'a> {
653    /// The parsed condition.
654    pub condition: Condition<'a>,
655    /// Fields in the then-branch.
656    pub then_fields: Vec<Field<'a>>,
657    /// Dependencies in the then-branch.
658    pub then_deps: Vec<Dependency<'a>>,
659    /// Fields in the else-branch.
660    pub else_fields: Vec<Field<'a>>,
661    /// Dependencies in the else-branch.
662    pub else_deps: Vec<Dependency<'a>>,
663    /// Nested conditionals in the then-branch.
664    pub then_conditionals: Vec<Conditional<'a>>,
665    /// Nested conditionals in the else-branch.
666    pub else_conditionals: Vec<Conditional<'a>>,
667    /// Back-reference to the CST conditional node.
668    pub cst_node: NodeId,
669}
670
671// ---------------------------------------------------------------------------
672// Component types
673// ---------------------------------------------------------------------------
674
675/// Shared fields across all component types (library, executable, etc.).
676#[derive(Debug, Clone, PartialEq, Eq)]
677pub struct ComponentFields<'a> {
678    /// Component name (`None` for the unnamed default library).
679    pub name: Option<&'a str>,
680    /// Back-reference to the CST section node.
681    pub cst_node: NodeId,
682    /// `import:` directives.
683    pub imports: Vec<&'a str>,
684    /// `build-depends` entries.
685    pub build_depends: Vec<Dependency<'a>>,
686    /// `other-modules` entries.
687    pub other_modules: Vec<&'a str>,
688    /// `hs-source-dirs` entries.
689    pub hs_source_dirs: Vec<&'a str>,
690    /// `default-language` value.
691    pub default_language: Option<&'a str>,
692    /// `default-extensions` entries.
693    pub default_extensions: Vec<&'a str>,
694    /// `ghc-options` entries.
695    pub ghc_options: Vec<&'a str>,
696    /// Fields not specifically parsed.
697    pub other_fields: Vec<Field<'a>>,
698    /// Conditional blocks within this component.
699    pub conditionals: Vec<Conditional<'a>>,
700}
701
702/// A library component.
703#[derive(Debug, Clone, PartialEq, Eq)]
704pub struct Library<'a> {
705    /// Shared component fields.
706    pub fields: ComponentFields<'a>,
707    /// `exposed-modules` entries.
708    pub exposed_modules: Vec<&'a str>,
709}
710
711/// An executable component.
712#[derive(Debug, Clone, PartialEq, Eq)]
713pub struct Executable<'a> {
714    /// Shared component fields.
715    pub fields: ComponentFields<'a>,
716    /// `main-is` value.
717    pub main_is: Option<&'a str>,
718}
719
720/// A test-suite component.
721#[derive(Debug, Clone, PartialEq, Eq)]
722pub struct TestSuite<'a> {
723    /// Shared component fields.
724    pub fields: ComponentFields<'a>,
725    /// `type` value (e.g. `exitcode-stdio-1.0`).
726    pub test_type: Option<&'a str>,
727    /// `main-is` value.
728    pub main_is: Option<&'a str>,
729}
730
731/// A benchmark component.
732#[derive(Debug, Clone, PartialEq, Eq)]
733pub struct Benchmark<'a> {
734    /// Shared component fields.
735    pub fields: ComponentFields<'a>,
736    /// `type` value (e.g. `exitcode-stdio-1.0`).
737    pub bench_type: Option<&'a str>,
738    /// `main-is` value.
739    pub main_is: Option<&'a str>,
740}
741
742/// A `common` stanza.
743#[derive(Debug, Clone, PartialEq, Eq)]
744pub struct CommonStanza<'a> {
745    /// The stanza name.
746    pub name: &'a str,
747    /// Shared component fields.
748    pub fields: ComponentFields<'a>,
749}
750
751/// A `flag` section.
752#[derive(Debug, Clone, PartialEq, Eq)]
753pub struct Flag<'a> {
754    /// The flag name.
755    pub name: &'a str,
756    /// Description, if present.
757    pub description: Option<&'a str>,
758    /// Default value, if present.
759    pub default: Option<bool>,
760    /// Whether the flag is manual.
761    pub manual: Option<bool>,
762    /// All other fields.
763    pub other_fields: Vec<Field<'a>>,
764    /// Back-reference to the CST section node.
765    pub cst_node: NodeId,
766}
767
768/// A `source-repository` section.
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub struct SourceRepository<'a> {
771    /// The kind (e.g. `"head"`, `"this"`).
772    pub kind: Option<&'a str>,
773    /// Repository type (e.g. `"git"`, `"darcs"`).
774    pub repo_type: Option<&'a str>,
775    /// Repository location URL.
776    pub location: Option<&'a str>,
777    /// Tag, if specified.
778    pub tag: Option<&'a str>,
779    /// Branch, if specified.
780    pub branch: Option<&'a str>,
781    /// Subdir, if specified.
782    pub subdir: Option<&'a str>,
783    /// All other fields.
784    pub other_fields: Vec<Field<'a>>,
785    /// Back-reference to the CST section node.
786    pub cst_node: NodeId,
787}
788
789// ---------------------------------------------------------------------------
790// Component enum (for uniform access)
791// ---------------------------------------------------------------------------
792
793/// A reference to any component type.
794#[derive(Debug, Clone)]
795pub enum Component<'a, 'b> {
796    Library(&'b Library<'a>),
797    Executable(&'b Executable<'a>),
798    TestSuite(&'b TestSuite<'a>),
799    Benchmark(&'b Benchmark<'a>),
800}
801
802impl<'a, 'b> Component<'a, 'b> {
803    /// Get the shared component fields.
804    pub fn fields(&self) -> &ComponentFields<'a> {
805        match self {
806            Component::Library(l) => &l.fields,
807            Component::Executable(e) => &e.fields,
808            Component::TestSuite(t) => &t.fields,
809            Component::Benchmark(b) => &b.fields,
810        }
811    }
812
813    /// Get the component name.
814    pub fn name(&self) -> Option<&'a str> {
815        self.fields().name
816    }
817
818    /// Get the CST node back-reference.
819    pub fn cst_node(&self) -> NodeId {
820        self.fields().cst_node
821    }
822}
823
824// ---------------------------------------------------------------------------
825// Top-level AST
826// ---------------------------------------------------------------------------
827
828/// The top-level AST for a parsed `.cabal` file.
829#[derive(Debug, Clone, PartialEq, Eq)]
830pub struct CabalFile<'a> {
831    /// Reference to the source text.
832    pub source: &'a str,
833    /// The `cabal-version` field.
834    pub cabal_version: Option<CabalVersion<'a>>,
835    /// Package name.
836    pub name: Option<&'a str>,
837    /// Package version.
838    pub version: Option<Version>,
839    /// License identifier.
840    pub license: Option<&'a str>,
841    /// One-line package summary.
842    pub synopsis: Option<&'a str>,
843    /// Longer package description.
844    pub description: Option<&'a str>,
845    /// Author name(s).
846    pub author: Option<&'a str>,
847    /// Maintainer email/name.
848    pub maintainer: Option<&'a str>,
849    /// Package homepage URL.
850    pub homepage: Option<&'a str>,
851    /// Bug tracker URL.
852    pub bug_reports: Option<&'a str>,
853    /// Category string.
854    pub category: Option<&'a str>,
855    /// Build type (`Simple`, `Configure`, `Make`, `Custom`).
856    pub build_type: Option<&'a str>,
857    /// `tested-with` field.
858    pub tested_with: Option<&'a str>,
859    /// Extra source files.
860    pub extra_source_files: Vec<&'a str>,
861    /// Top-level fields not specifically parsed.
862    pub other_fields: Vec<Field<'a>>,
863    /// `common` stanzas.
864    pub common_stanzas: Vec<CommonStanza<'a>>,
865    /// `flag` sections.
866    pub flags: Vec<Flag<'a>>,
867    /// The unnamed default library (if present).
868    pub library: Option<Library<'a>>,
869    /// Named internal libraries.
870    pub named_libraries: Vec<Library<'a>>,
871    /// Executable components.
872    pub executables: Vec<Executable<'a>>,
873    /// Test suite components.
874    pub test_suites: Vec<TestSuite<'a>>,
875    /// Benchmark components.
876    pub benchmarks: Vec<Benchmark<'a>>,
877    /// Source repository sections.
878    pub source_repositories: Vec<SourceRepository<'a>>,
879    /// Back-reference to the CST root node.
880    pub cst_root: NodeId,
881}
882
883impl<'a> CabalFile<'a> {
884    /// Collect all dependencies across all components, including conditional
885    /// blocks.
886    pub fn all_dependencies(&self) -> Vec<&Dependency<'a>> {
887        let mut deps = Vec::new();
888
889        if let Some(ref lib) = self.library {
890            collect_component_deps(&lib.fields, &mut deps);
891        }
892        for lib in &self.named_libraries {
893            collect_component_deps(&lib.fields, &mut deps);
894        }
895        for exe in &self.executables {
896            collect_component_deps(&exe.fields, &mut deps);
897        }
898        for ts in &self.test_suites {
899            collect_component_deps(&ts.fields, &mut deps);
900        }
901        for bm in &self.benchmarks {
902            collect_component_deps(&bm.fields, &mut deps);
903        }
904        for cs in &self.common_stanzas {
905            collect_component_deps(&cs.fields, &mut deps);
906        }
907
908        deps
909    }
910
911    /// Return references to all components (library, executables, test suites,
912    /// benchmarks).
913    pub fn all_components(&self) -> Vec<Component<'a, '_>> {
914        let mut comps = Vec::new();
915        if let Some(ref lib) = self.library {
916            comps.push(Component::Library(lib));
917        }
918        for lib in &self.named_libraries {
919            comps.push(Component::Library(lib));
920        }
921        for exe in &self.executables {
922            comps.push(Component::Executable(exe));
923        }
924        for ts in &self.test_suites {
925            comps.push(Component::TestSuite(ts));
926        }
927        for bm in &self.benchmarks {
928            comps.push(Component::Benchmark(bm));
929        }
930        comps
931    }
932
933    /// Find a component by name.
934    ///
935    /// The unnamed library can be found by passing `"library"`.
936    pub fn find_component(&self, name: &str) -> Option<Component<'a, '_>> {
937        if let Some(ref lib) = self.library {
938            if name == "library" || lib.fields.name == Some(name) {
939                return Some(Component::Library(lib));
940            }
941        }
942        for lib in &self.named_libraries {
943            if lib.fields.name == Some(name) {
944                return Some(Component::Library(lib));
945            }
946        }
947        for exe in &self.executables {
948            if exe.fields.name == Some(name) {
949                return Some(Component::Executable(exe));
950            }
951        }
952        for ts in &self.test_suites {
953            if ts.fields.name == Some(name) {
954                return Some(Component::TestSuite(ts));
955            }
956        }
957        for bm in &self.benchmarks {
958            if bm.fields.name == Some(name) {
959                return Some(Component::Benchmark(bm));
960            }
961        }
962        None
963    }
964}
965
966/// Collect deps from a component's fields, including conditional deps.
967fn collect_component_deps<'a, 'b>(
968    fields: &'b ComponentFields<'a>,
969    deps: &mut Vec<&'b Dependency<'a>>,
970) {
971    for d in &fields.build_depends {
972        deps.push(d);
973    }
974    collect_conditional_deps(&fields.conditionals, deps);
975}
976
977fn collect_conditional_deps<'a, 'b>(
978    conditionals: &'b [Conditional<'a>],
979    deps: &mut Vec<&'b Dependency<'a>>,
980) {
981    for cond in conditionals {
982        for d in &cond.then_deps {
983            deps.push(d);
984        }
985        for d in &cond.else_deps {
986            deps.push(d);
987        }
988        collect_conditional_deps(&cond.then_conditionals, deps);
989        collect_conditional_deps(&cond.else_conditionals, deps);
990    }
991}
992
993// ---------------------------------------------------------------------------
994// AST derivation from CST
995// ---------------------------------------------------------------------------
996
997/// Derive a typed AST from a parsed CST.
998///
999/// Walks the CST root's children, matching field names to known metadata
1000/// fields, and section keywords to component types. Within each section,
1001/// fields are parsed into typed representations.
1002pub fn derive_ast<'a>(cst: &'a CabalCst) -> CabalFile<'a> {
1003    let source = cst.source.as_str();
1004    let mut file = CabalFile {
1005        source,
1006        cabal_version: None,
1007        name: None,
1008        version: None,
1009        license: None,
1010        synopsis: None,
1011        description: None,
1012        author: None,
1013        maintainer: None,
1014        homepage: None,
1015        bug_reports: None,
1016        category: None,
1017        build_type: None,
1018        tested_with: None,
1019        extra_source_files: Vec::new(),
1020        other_fields: Vec::new(),
1021        common_stanzas: Vec::new(),
1022        flags: Vec::new(),
1023        library: None,
1024        named_libraries: Vec::new(),
1025        executables: Vec::new(),
1026        test_suites: Vec::new(),
1027        benchmarks: Vec::new(),
1028        source_repositories: Vec::new(),
1029        cst_root: cst.root,
1030    };
1031
1032    // Walk all nodes recursively. The CST parser may nest top-level sections
1033    // inside each other when they are at indent 0, so we cannot rely on only
1034    // examining direct children of root.
1035    collect_ast_nodes(cst, cst.root, &mut file);
1036
1037    file
1038}
1039
1040/// Recursively walk the CST node tree and collect top-level fields and
1041/// sections into the CabalFile. This handles the case where the CST parser
1042/// nests sibling sections inside each other (due to indent-based parsing
1043/// with all top-level sections at column 0).
1044fn collect_ast_nodes<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
1045    let node = cst.node(node_id);
1046
1047    match node.kind {
1048        CstNodeKind::Root => {
1049            // Process direct children.
1050            let children: Vec<NodeId> = node.children.clone();
1051            for child_id in children {
1052                collect_ast_nodes(cst, child_id, file);
1053            }
1054        }
1055        CstNodeKind::Field if cst.node(node_id).parent == Some(cst.root) => {
1056            derive_top_level_field(cst, node_id, file);
1057        }
1058        CstNodeKind::Field => {}
1059        CstNodeKind::Section => {
1060            // Check if this is a known section type (library, executable, etc.).
1061            let source = cst.source.as_str();
1062            let is_top_level_section = if let Some(ref kw_span) = node.section_keyword {
1063                let kw = kw_span.slice(source).to_ascii_lowercase();
1064                matches!(
1065                    kw.as_str(),
1066                    "library"
1067                        | "executable"
1068                        | "test-suite"
1069                        | "benchmark"
1070                        | "common"
1071                        | "flag"
1072                        | "source-repository"
1073                )
1074            } else {
1075                false
1076            };
1077
1078            if is_top_level_section {
1079                derive_section(cst, node_id, file);
1080                // Also recursively check this section's children for more
1081                // sections that may have been nested by the parser.
1082                let children: Vec<NodeId> = cst.node(node_id).children.clone();
1083                for child_id in children {
1084                    let child = cst.node(child_id);
1085                    if child.kind == CstNodeKind::Section {
1086                        collect_ast_nodes(cst, child_id, file);
1087                    }
1088                }
1089            }
1090        }
1091        _ => {}
1092    }
1093}
1094
1095/// Extract the full value text for a field node, including continuation
1096/// (ValueLine) children. The result is trimmed.
1097fn field_full_value(cst: &CabalCst, node_id: NodeId) -> String {
1098    let node = cst.node(node_id);
1099    let source = cst.source.as_str();
1100
1101    let mut value = String::new();
1102
1103    // First line value.
1104    if let Some(ref val_span) = node.field_value {
1105        value.push_str(val_span.slice(source).trim());
1106    }
1107
1108    // Continuation lines.
1109    for &child_id in &node.children {
1110        let child = cst.node(child_id);
1111        if child.kind == CstNodeKind::ValueLine {
1112            let line_text = child.content_span.slice(source).trim();
1113            if !line_text.is_empty() {
1114                if !value.is_empty() {
1115                    value.push('\n');
1116                }
1117                value.push_str(line_text);
1118            }
1119        }
1120    }
1121
1122    value
1123}
1124
1125/// Get the field value as a borrowed str reference (trimmed).
1126/// Checks the first line, then falls back to the first continuation line.
1127/// Handles fields like `name:\n  hedgehog`.
1128fn field_first_line_value(cst: &CabalCst, node_id: NodeId) -> Option<&str> {
1129    let node = cst.node(node_id);
1130    let source = cst.source.as_str();
1131
1132    // Try the first-line value.
1133    if let Some(ref val_span) = node.field_value {
1134        let v = val_span.slice(source).trim();
1135        if !v.is_empty() {
1136            return Some(v);
1137        }
1138    }
1139
1140    // Fall back to first non-empty continuation line.
1141    for &child_id in &node.children {
1142        let child = cst.node(child_id);
1143        if child.kind == CstNodeKind::ValueLine {
1144            let v = child.content_span.slice(source).trim();
1145            if !v.is_empty() {
1146                return Some(v);
1147            }
1148        }
1149    }
1150
1151    None
1152}
1153
1154/// Parse a whitespace/newline-separated list from a field value.
1155/// Used for `exposed-modules`, `other-modules`, `hs-source-dirs`,
1156/// `default-extensions`, etc.
1157fn parse_list_field(cst: &CabalCst, node_id: NodeId) -> Vec<&str> {
1158    let node = cst.node(node_id);
1159    let source = cst.source.as_str();
1160    let mut items = Vec::new();
1161
1162    // First line.
1163    if let Some(ref val_span) = node.field_value {
1164        let text = val_span.slice(source).trim();
1165        for item in split_list_items(text) {
1166            if !item.is_empty() {
1167                items.push(item);
1168            }
1169        }
1170    }
1171
1172    // Continuation lines.
1173    for &child_id in &node.children {
1174        let child = cst.node(child_id);
1175        if child.kind == CstNodeKind::ValueLine {
1176            let text = child.content_span.slice(source).trim();
1177            for item in split_list_items(text) {
1178                if !item.is_empty() {
1179                    items.push(item);
1180                }
1181            }
1182        }
1183    }
1184
1185    items
1186}
1187
1188/// Split a line into list items, handling commas as separators and stripping
1189/// leading/trailing commas.
1190fn split_list_items(text: &str) -> Vec<&str> {
1191    let mut items = Vec::new();
1192    if text.contains(',') {
1193        for part in text.split(',') {
1194            let trimmed = part.trim();
1195            if !trimmed.is_empty() {
1196                items.push(trimmed);
1197            }
1198        }
1199    } else {
1200        // Space-separated.
1201        for part in text.split_whitespace() {
1202            items.push(part);
1203        }
1204    }
1205    items
1206}
1207
1208/// Parse `ghc-options` value: space-separated tokens, possibly multi-line.
1209fn parse_ghc_options(cst: &CabalCst, node_id: NodeId) -> Vec<&str> {
1210    let node = cst.node(node_id);
1211    let source = cst.source.as_str();
1212    let mut opts = Vec::new();
1213
1214    if let Some(ref val_span) = node.field_value {
1215        for opt in val_span.slice(source).split_whitespace() {
1216            opts.push(opt);
1217        }
1218    }
1219
1220    for &child_id in &node.children {
1221        let child = cst.node(child_id);
1222        if child.kind == CstNodeKind::ValueLine {
1223            for opt in child.content_span.slice(source).split_whitespace() {
1224                opts.push(opt);
1225            }
1226        }
1227    }
1228
1229    opts
1230}
1231
1232/// Parse dependencies from a `build-depends` field node.
1233fn parse_build_depends<'a>(cst: &'a CabalCst, node_id: NodeId) -> Vec<Dependency<'a>> {
1234    let node = cst.node(node_id);
1235    let source = cst.source.as_str();
1236    let mut deps = Vec::new();
1237
1238    // First line value.
1239    if let Some(ref val_span) = node.field_value {
1240        let text = val_span.slice(source).trim();
1241        deps.extend(parse_dependencies_from_text(text, node_id));
1242    }
1243
1244    // Continuation lines.
1245    for &child_id in &node.children {
1246        let child = cst.node(child_id);
1247        if child.kind == CstNodeKind::ValueLine {
1248            let text = child.content_span.slice(source).trim();
1249            if !text.is_empty() {
1250                // Use the child's NodeId so back-references point to the
1251                // specific ValueLine.
1252                deps.extend(parse_dependencies_from_text(text, child_id));
1253            }
1254        }
1255    }
1256
1257    deps
1258}
1259
1260/// Derive a top-level field into the CabalFile metadata.
1261fn derive_top_level_field<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
1262    let node = cst.node(node_id);
1263    let source = cst.source.as_str();
1264
1265    let raw_name = match node.field_name {
1266        Some(ref span) => span.slice(source),
1267        None => return,
1268    };
1269    let canon = canonicalize_field_name(raw_name);
1270
1271    match canon.as_str() {
1272        "cabal-version" => {
1273            let raw = field_first_line_value(cst, node_id).unwrap_or("");
1274            // Strip leading `>=` that some old files use.
1275            let version_str = raw.strip_prefix(">=").unwrap_or(raw).trim();
1276            file.cabal_version = Some(CabalVersion {
1277                raw,
1278                version: Version::parse(version_str),
1279                cst_node: node_id,
1280            });
1281        }
1282        "name" => {
1283            file.name = field_first_line_value(cst, node_id);
1284        }
1285        "version" => {
1286            let raw = field_first_line_value(cst, node_id).unwrap_or("");
1287            file.version = Version::parse(raw);
1288        }
1289        "license" => {
1290            file.license = field_first_line_value(cst, node_id);
1291        }
1292        "synopsis" => {
1293            file.synopsis = field_first_line_value(cst, node_id);
1294        }
1295        "description" => {
1296            // Description can be multi-line; we store a reference to the first
1297            // line and let callers use `field_full_value` if they need all of
1298            // it. For the AST we just capture the first line.
1299            file.description = field_first_line_value(cst, node_id);
1300        }
1301        "author" => {
1302            file.author = field_first_line_value(cst, node_id);
1303        }
1304        "maintainer" => {
1305            file.maintainer = field_first_line_value(cst, node_id);
1306        }
1307        "homepage" => {
1308            file.homepage = field_first_line_value(cst, node_id);
1309        }
1310        "bug-reports" => {
1311            file.bug_reports = field_first_line_value(cst, node_id);
1312        }
1313        "category" => {
1314            file.category = field_first_line_value(cst, node_id);
1315        }
1316        "build-type" => {
1317            file.build_type = field_first_line_value(cst, node_id);
1318        }
1319        "tested-with" => {
1320            file.tested_with = field_first_line_value(cst, node_id);
1321        }
1322        "extra-source-files" | "extra-doc-files" => {
1323            file.extra_source_files
1324                .extend(parse_list_field(cst, node_id));
1325        }
1326        _ => {
1327            let value = field_full_value(cst, node_id);
1328            file.other_fields.push(Field {
1329                name: canon,
1330                raw_name,
1331                value,
1332                cst_node: node_id,
1333            });
1334        }
1335    }
1336}
1337
1338/// Derive a section (library, executable, etc.) into the CabalFile.
1339fn derive_section<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
1340    let node = cst.node(node_id);
1341    let source = cst.source.as_str();
1342
1343    let keyword = match node.section_keyword {
1344        Some(ref span) => span.slice(source),
1345        None => return,
1346    };
1347    let section_arg = node.section_arg.map(|span| span.slice(source));
1348    let keyword_lower = keyword.to_ascii_lowercase();
1349
1350    match keyword_lower.as_str() {
1351        "library" => {
1352            let lib = derive_library(cst, node_id, section_arg);
1353            if section_arg.is_some() {
1354                file.named_libraries.push(lib);
1355            } else {
1356                file.library = Some(lib);
1357            }
1358        }
1359        "executable" => {
1360            let exe = derive_executable(cst, node_id, section_arg);
1361            file.executables.push(exe);
1362        }
1363        "test-suite" => {
1364            let ts = derive_test_suite(cst, node_id, section_arg);
1365            file.test_suites.push(ts);
1366        }
1367        "benchmark" => {
1368            let bm = derive_benchmark(cst, node_id, section_arg);
1369            file.benchmarks.push(bm);
1370        }
1371        "common" => {
1372            if let Some(name) = section_arg {
1373                let cs = derive_common_stanza(cst, node_id, name);
1374                file.common_stanzas.push(cs);
1375            }
1376        }
1377        "flag" => {
1378            if let Some(name) = section_arg {
1379                let flag = derive_flag(cst, node_id, name);
1380                file.flags.push(flag);
1381            }
1382        }
1383        "source-repository" => {
1384            let sr = derive_source_repository(cst, node_id, section_arg);
1385            file.source_repositories.push(sr);
1386        }
1387        _ => {
1388            // Unknown section type: ignore for now.
1389        }
1390    }
1391}
1392
1393/// Create default empty `ComponentFields`.
1394fn empty_component_fields<'a>(name: Option<&'a str>, cst_node: NodeId) -> ComponentFields<'a> {
1395    ComponentFields {
1396        name,
1397        cst_node,
1398        imports: Vec::new(),
1399        build_depends: Vec::new(),
1400        other_modules: Vec::new(),
1401        hs_source_dirs: Vec::new(),
1402        default_language: None,
1403        default_extensions: Vec::new(),
1404        ghc_options: Vec::new(),
1405        other_fields: Vec::new(),
1406        conditionals: Vec::new(),
1407    }
1408}
1409
1410/// Populate `ComponentFields` from the children of a section node.
1411fn populate_component_fields<'a>(
1412    cst: &'a CabalCst,
1413    section_id: NodeId,
1414    fields: &mut ComponentFields<'a>,
1415) {
1416    let section = cst.node(section_id);
1417    let source = cst.source.as_str();
1418
1419    for &child_id in &section.children {
1420        let child = cst.node(child_id);
1421        match child.kind {
1422            CstNodeKind::Field => {
1423                let raw_name = match child.field_name {
1424                    Some(ref span) => span.slice(source),
1425                    None => continue,
1426                };
1427                let canon = canonicalize_field_name(raw_name);
1428
1429                match canon.as_str() {
1430                    "build-depends" => {
1431                        fields
1432                            .build_depends
1433                            .extend(parse_build_depends(cst, child_id));
1434                    }
1435                    "exposed-modules" => {
1436                        // Handled by caller if Library.
1437                        // We still parse here and caller picks it up.
1438                    }
1439                    "other-modules" => {
1440                        fields.other_modules.extend(parse_list_field(cst, child_id));
1441                    }
1442                    "hs-source-dirs" => {
1443                        fields
1444                            .hs_source_dirs
1445                            .extend(parse_list_field(cst, child_id));
1446                    }
1447                    "default-language" => {
1448                        fields.default_language = field_first_line_value(cst, child_id);
1449                    }
1450                    "default-extensions" | "extensions" => {
1451                        fields
1452                            .default_extensions
1453                            .extend(parse_list_field(cst, child_id));
1454                    }
1455                    "ghc-options" => {
1456                        fields.ghc_options.extend(parse_ghc_options(cst, child_id));
1457                    }
1458                    _ => {
1459                        let value = field_full_value(cst, child_id);
1460                        fields.other_fields.push(Field {
1461                            name: canon,
1462                            raw_name,
1463                            value,
1464                            cst_node: child_id,
1465                        });
1466                    }
1467                }
1468            }
1469            CstNodeKind::Import => {
1470                if let Some(ref val_span) = child.field_value {
1471                    let val = val_span.slice(source).trim();
1472                    if !val.is_empty() {
1473                        // imports can be comma-separated
1474                        for item in val.split(',') {
1475                            let item = item.trim();
1476                            if !item.is_empty() {
1477                                fields.imports.push(item);
1478                            }
1479                        }
1480                    }
1481                }
1482            }
1483            CstNodeKind::Conditional => {
1484                let cond = derive_conditional(cst, child_id);
1485                fields.conditionals.push(cond);
1486            }
1487            // Comments, blank lines, value lines: skip for AST.
1488            _ => {}
1489        }
1490    }
1491}
1492
1493/// Derive a conditional block from a CST Conditional node.
1494fn derive_conditional<'a>(cst: &'a CabalCst, node_id: NodeId) -> Conditional<'a> {
1495    let node = cst.node(node_id);
1496    let source = cst.source.as_str();
1497
1498    // Parse condition expression.
1499    let condition = match node.condition_expr {
1500        Some(ref span) => parse_condition(span.slice(source)),
1501        None => Condition::Raw(""),
1502    };
1503
1504    let mut cond = Conditional {
1505        condition,
1506        then_fields: Vec::new(),
1507        then_deps: Vec::new(),
1508        else_fields: Vec::new(),
1509        else_deps: Vec::new(),
1510        then_conditionals: Vec::new(),
1511        else_conditionals: Vec::new(),
1512        cst_node: node_id,
1513    };
1514
1515    // Process children: then-block items, then the ElseBlock.
1516    for &child_id in &node.children {
1517        let child = cst.node(child_id);
1518        match child.kind {
1519            CstNodeKind::Field => {
1520                let raw_name = match child.field_name {
1521                    Some(ref span) => span.slice(source),
1522                    None => continue,
1523                };
1524                let canon = canonicalize_field_name(raw_name);
1525
1526                if canon == "build-depends" {
1527                    cond.then_deps.extend(parse_build_depends(cst, child_id));
1528                } else {
1529                    let value = field_full_value(cst, child_id);
1530                    cond.then_fields.push(Field {
1531                        name: canon,
1532                        raw_name,
1533                        value,
1534                        cst_node: child_id,
1535                    });
1536                }
1537            }
1538            CstNodeKind::Conditional => {
1539                cond.then_conditionals
1540                    .push(derive_conditional(cst, child_id));
1541            }
1542            CstNodeKind::ElseBlock => {
1543                // Process else block children.
1544                for &else_child_id in &child.children {
1545                    let else_child = cst.node(else_child_id);
1546                    match else_child.kind {
1547                        CstNodeKind::Field => {
1548                            let raw_name = match else_child.field_name {
1549                                Some(ref span) => span.slice(source),
1550                                None => continue,
1551                            };
1552                            let canon = canonicalize_field_name(raw_name);
1553
1554                            if canon == "build-depends" {
1555                                cond.else_deps
1556                                    .extend(parse_build_depends(cst, else_child_id));
1557                            } else {
1558                                let value = field_full_value(cst, else_child_id);
1559                                cond.else_fields.push(Field {
1560                                    name: canon,
1561                                    raw_name,
1562                                    value,
1563                                    cst_node: else_child_id,
1564                                });
1565                            }
1566                        }
1567                        CstNodeKind::Conditional => {
1568                            cond.else_conditionals
1569                                .push(derive_conditional(cst, else_child_id));
1570                        }
1571                        _ => {}
1572                    }
1573                }
1574            }
1575            _ => {}
1576        }
1577    }
1578
1579    cond
1580}
1581
1582/// Derive a Library from a CST section node.
1583fn derive_library<'a>(cst: &'a CabalCst, node_id: NodeId, name: Option<&'a str>) -> Library<'a> {
1584    let mut fields = empty_component_fields(name, node_id);
1585    populate_component_fields(cst, node_id, &mut fields);
1586
1587    // Extract exposed-modules from section children (since populate_component_fields
1588    // skips it for the generic path).
1589    let exposed_modules = extract_exposed_modules(cst, node_id);
1590
1591    Library {
1592        fields,
1593        exposed_modules,
1594    }
1595}
1596
1597/// Extract `exposed-modules` from a section's children.
1598fn extract_exposed_modules(cst: &CabalCst, section_id: NodeId) -> Vec<&str> {
1599    let section = cst.node(section_id);
1600    let source = cst.source.as_str();
1601    let mut modules = Vec::new();
1602
1603    for &child_id in &section.children {
1604        let child = cst.node(child_id);
1605        if child.kind == CstNodeKind::Field {
1606            if let Some(ref name_span) = child.field_name {
1607                let canon = canonicalize_field_name(name_span.slice(source));
1608                if canon == "exposed-modules" {
1609                    modules.extend(parse_list_field(cst, child_id));
1610                }
1611            }
1612        }
1613    }
1614
1615    modules
1616}
1617
1618/// Derive an Executable from a CST section node.
1619fn derive_executable<'a>(
1620    cst: &'a CabalCst,
1621    node_id: NodeId,
1622    name: Option<&'a str>,
1623) -> Executable<'a> {
1624    let main_is = find_field_value_in_section(cst, node_id, "main-is");
1625
1626    let mut fields = empty_component_fields(name, node_id);
1627    populate_component_fields(cst, node_id, &mut fields);
1628    remove_field_by_name(&mut fields.other_fields, "main-is");
1629
1630    Executable { fields, main_is }
1631}
1632
1633/// Derive a TestSuite from a CST section node.
1634fn derive_test_suite<'a>(
1635    cst: &'a CabalCst,
1636    node_id: NodeId,
1637    name: Option<&'a str>,
1638) -> TestSuite<'a> {
1639    let test_type = find_field_value_in_section(cst, node_id, "type");
1640    let main_is = find_field_value_in_section(cst, node_id, "main-is");
1641
1642    let mut fields = empty_component_fields(name, node_id);
1643    populate_component_fields(cst, node_id, &mut fields);
1644    remove_field_by_name(&mut fields.other_fields, "type");
1645    remove_field_by_name(&mut fields.other_fields, "main-is");
1646
1647    TestSuite {
1648        fields,
1649        test_type,
1650        main_is,
1651    }
1652}
1653
1654/// Derive a Benchmark from a CST section node.
1655fn derive_benchmark<'a>(
1656    cst: &'a CabalCst,
1657    node_id: NodeId,
1658    name: Option<&'a str>,
1659) -> Benchmark<'a> {
1660    let bench_type = find_field_value_in_section(cst, node_id, "type");
1661    let main_is = find_field_value_in_section(cst, node_id, "main-is");
1662
1663    let mut fields = empty_component_fields(name, node_id);
1664    populate_component_fields(cst, node_id, &mut fields);
1665    remove_field_by_name(&mut fields.other_fields, "type");
1666    remove_field_by_name(&mut fields.other_fields, "main-is");
1667
1668    Benchmark {
1669        fields,
1670        bench_type,
1671        main_is,
1672    }
1673}
1674
1675/// Derive a CommonStanza from a CST section node.
1676fn derive_common_stanza<'a>(cst: &'a CabalCst, node_id: NodeId, name: &'a str) -> CommonStanza<'a> {
1677    let mut fields = empty_component_fields(Some(name), node_id);
1678    populate_component_fields(cst, node_id, &mut fields);
1679
1680    CommonStanza { name, fields }
1681}
1682
1683/// Derive a Flag from a CST section node.
1684fn derive_flag<'a>(cst: &'a CabalCst, node_id: NodeId, name: &'a str) -> Flag<'a> {
1685    let section = cst.node(node_id);
1686    let source = cst.source.as_str();
1687
1688    let mut description = None;
1689    let mut default = None;
1690    let mut manual = None;
1691    let mut other_fields = Vec::new();
1692
1693    for &child_id in &section.children {
1694        let child = cst.node(child_id);
1695        if child.kind == CstNodeKind::Field {
1696            let raw_name = match child.field_name {
1697                Some(ref span) => span.slice(source),
1698                None => continue,
1699            };
1700            let canon = canonicalize_field_name(raw_name);
1701
1702            match canon.as_str() {
1703                "description" => {
1704                    description = field_first_line_value(cst, child_id);
1705                }
1706                "default" => {
1707                    if let Some(val) = field_first_line_value(cst, child_id) {
1708                        let lower = val.to_ascii_lowercase();
1709                        default = Some(lower == "true");
1710                    }
1711                }
1712                "manual" => {
1713                    if let Some(val) = field_first_line_value(cst, child_id) {
1714                        let lower = val.to_ascii_lowercase();
1715                        manual = Some(lower == "true");
1716                    }
1717                }
1718                _ => {
1719                    let value = field_full_value(cst, child_id);
1720                    other_fields.push(Field {
1721                        name: canon,
1722                        raw_name,
1723                        value,
1724                        cst_node: child_id,
1725                    });
1726                }
1727            }
1728        }
1729    }
1730
1731    Flag {
1732        name,
1733        description,
1734        default,
1735        manual,
1736        other_fields,
1737        cst_node: node_id,
1738    }
1739}
1740
1741/// Derive a SourceRepository from a CST section node.
1742fn derive_source_repository<'a>(
1743    cst: &'a CabalCst,
1744    node_id: NodeId,
1745    kind: Option<&'a str>,
1746) -> SourceRepository<'a> {
1747    let section = cst.node(node_id);
1748    let source = cst.source.as_str();
1749
1750    let mut repo_type = None;
1751    let mut location = None;
1752    let mut tag = None;
1753    let mut branch = None;
1754    let mut subdir = None;
1755    let mut other_fields = Vec::new();
1756
1757    for &child_id in &section.children {
1758        let child = cst.node(child_id);
1759        if child.kind == CstNodeKind::Field {
1760            let raw_name = match child.field_name {
1761                Some(ref span) => span.slice(source),
1762                None => continue,
1763            };
1764            let canon = canonicalize_field_name(raw_name);
1765
1766            match canon.as_str() {
1767                "type" => {
1768                    repo_type = field_first_line_value(cst, child_id);
1769                }
1770                "location" => {
1771                    location = field_first_line_value(cst, child_id);
1772                }
1773                "tag" => {
1774                    tag = field_first_line_value(cst, child_id);
1775                }
1776                "branch" => {
1777                    branch = field_first_line_value(cst, child_id);
1778                }
1779                "subdir" => {
1780                    subdir = field_first_line_value(cst, child_id);
1781                }
1782                _ => {
1783                    let value = field_full_value(cst, child_id);
1784                    other_fields.push(Field {
1785                        name: canon,
1786                        raw_name,
1787                        value,
1788                        cst_node: child_id,
1789                    });
1790                }
1791            }
1792        }
1793    }
1794
1795    SourceRepository {
1796        kind,
1797        repo_type,
1798        location,
1799        tag,
1800        branch,
1801        subdir,
1802        other_fields,
1803        cst_node: node_id,
1804    }
1805}
1806
1807/// Remove a field by canonicalized name from `other_fields`.
1808fn remove_field_by_name(fields: &mut Vec<Field<'_>>, canonical_name: &str) {
1809    fields.retain(|f| f.name != canonical_name);
1810}
1811
1812/// Look up a field by canonicalized name in a section's children and return
1813/// its first-line value.
1814fn find_field_value_in_section<'a>(
1815    cst: &'a CabalCst,
1816    section_id: NodeId,
1817    target_canon: &str,
1818) -> Option<&'a str> {
1819    let section = cst.node(section_id);
1820    let source = cst.source.as_str();
1821
1822    for &child_id in &section.children {
1823        let child = cst.node(child_id);
1824        if child.kind == CstNodeKind::Field {
1825            if let Some(ref name_span) = child.field_name {
1826                let canon = canonicalize_field_name(name_span.slice(source));
1827                if canon == target_canon {
1828                    return field_first_line_value(cst, child_id);
1829                }
1830            }
1831        }
1832    }
1833    None
1834}
1835
1836// ---------------------------------------------------------------------------
1837// Tests
1838// ---------------------------------------------------------------------------
1839
1840#[cfg(test)]
1841mod tests {
1842    use super::*;
1843
1844    /// Parse source and return the ParseResult. Callers derive the AST from it.
1845    fn do_parse(source: &str) -> crate::parse::ParseResult {
1846        crate::parse::parse(source)
1847    }
1848
1849    // -- Version parsing tests ------------------------------------------------
1850
1851    #[test]
1852    fn version_parse_simple() {
1853        let v = Version::parse("0.1.0.0").unwrap();
1854        assert_eq!(v.components, vec![0, 1, 0, 0]);
1855    }
1856
1857    #[test]
1858    fn version_parse_two_components() {
1859        let v = Version::parse("4.14").unwrap();
1860        assert_eq!(v.components, vec![4, 14]);
1861    }
1862
1863    #[test]
1864    fn version_parse_single() {
1865        let v = Version::parse("5").unwrap();
1866        assert_eq!(v.components, vec![5]);
1867    }
1868
1869    #[test]
1870    fn version_parse_empty() {
1871        assert!(Version::parse("").is_none());
1872    }
1873
1874    #[test]
1875    fn version_parse_invalid() {
1876        assert!(Version::parse("abc").is_none());
1877        assert!(Version::parse("1.2.abc").is_none());
1878    }
1879
1880    #[test]
1881    fn version_display() {
1882        let v = Version {
1883            components: vec![1, 2, 3, 0],
1884        };
1885        assert_eq!(v.to_string(), "1.2.3.0");
1886    }
1887
1888    // -- Version range parsing tests ------------------------------------------
1889
1890    #[test]
1891    fn version_range_gte() {
1892        let vr = parse_version_range(">=4.14").unwrap();
1893        assert_eq!(
1894            vr,
1895            VersionRange::Gte(Version {
1896                components: vec![4, 14]
1897            })
1898        );
1899    }
1900
1901    #[test]
1902    fn version_range_lt() {
1903        let vr = parse_version_range("<5").unwrap();
1904        assert_eq!(
1905            vr,
1906            VersionRange::Lt(Version {
1907                components: vec![5]
1908            })
1909        );
1910    }
1911
1912    #[test]
1913    fn version_range_major_bound() {
1914        let vr = parse_version_range("^>=2.2").unwrap();
1915        assert_eq!(
1916            vr,
1917            VersionRange::MajorBound(Version {
1918                components: vec![2, 2]
1919            })
1920        );
1921    }
1922
1923    #[test]
1924    fn version_range_eq() {
1925        let vr = parse_version_range("==1.0").unwrap();
1926        assert_eq!(
1927            vr,
1928            VersionRange::Eq(Version {
1929                components: vec![1, 0]
1930            })
1931        );
1932    }
1933
1934    #[test]
1935    fn version_range_and() {
1936        let vr = parse_version_range(">=4.14 && <5").unwrap();
1937        assert_eq!(
1938            vr,
1939            VersionRange::And(
1940                Box::new(VersionRange::Gte(Version {
1941                    components: vec![4, 14]
1942                })),
1943                Box::new(VersionRange::Lt(Version {
1944                    components: vec![5]
1945                })),
1946            )
1947        );
1948    }
1949
1950    #[test]
1951    fn version_range_or() {
1952        let vr = parse_version_range(">=2.0 || ==1.9").unwrap();
1953        assert_eq!(
1954            vr,
1955            VersionRange::Or(
1956                Box::new(VersionRange::Gte(Version {
1957                    components: vec![2, 0]
1958                })),
1959                Box::new(VersionRange::Eq(Version {
1960                    components: vec![1, 9]
1961                })),
1962            )
1963        );
1964    }
1965
1966    #[test]
1967    fn version_range_complex_and() {
1968        let vr = parse_version_range(">=2.0 && <2.2").unwrap();
1969        assert_eq!(
1970            vr,
1971            VersionRange::And(
1972                Box::new(VersionRange::Gte(Version {
1973                    components: vec![2, 0]
1974                })),
1975                Box::new(VersionRange::Lt(Version {
1976                    components: vec![2, 2]
1977                })),
1978            )
1979        );
1980    }
1981
1982    #[test]
1983    fn version_range_empty() {
1984        assert!(parse_version_range("").is_none());
1985    }
1986
1987    // -- Canonicalize field name tests ----------------------------------------
1988
1989    #[test]
1990    fn canonicalize_mixed_case() {
1991        assert_eq!(canonicalize_field_name("Build-Depends"), "build-depends");
1992    }
1993
1994    #[test]
1995    fn canonicalize_underscore() {
1996        assert_eq!(canonicalize_field_name("build_depends"), "build-depends");
1997    }
1998
1999    #[test]
2000    fn canonicalize_already_canonical() {
2001        assert_eq!(canonicalize_field_name("build-depends"), "build-depends");
2002    }
2003
2004    // -- Dependency parsing tests ---------------------------------------------
2005
2006    #[test]
2007    fn parse_dep_no_version() {
2008        let dep = parse_single_dependency("base", NodeId(0)).unwrap();
2009        assert_eq!(dep.package, "base");
2010        assert!(dep.version_range.is_none());
2011    }
2012
2013    #[test]
2014    fn parse_dep_with_version() {
2015        let dep = parse_single_dependency("aeson ^>=2.2", NodeId(0)).unwrap();
2016        assert_eq!(dep.package, "aeson");
2017        assert_eq!(
2018            dep.version_range,
2019            Some(VersionRange::MajorBound(Version {
2020                components: vec![2, 2]
2021            }))
2022        );
2023    }
2024
2025    #[test]
2026    fn parse_dep_with_range() {
2027        let dep = parse_single_dependency("base >=4.14 && <5", NodeId(0)).unwrap();
2028        assert_eq!(dep.package, "base");
2029        assert_eq!(
2030            dep.version_range,
2031            Some(VersionRange::And(
2032                Box::new(VersionRange::Gte(Version {
2033                    components: vec![4, 14]
2034                })),
2035                Box::new(VersionRange::Lt(Version {
2036                    components: vec![5]
2037                })),
2038            ))
2039        );
2040    }
2041
2042    #[test]
2043    fn parse_deps_comma_separated() {
2044        let deps = parse_dependencies_from_text("base >=4.14, text >=2.0, aeson ^>=2.2", NodeId(0));
2045        assert_eq!(deps.len(), 3);
2046        assert_eq!(deps[0].package, "base");
2047        assert_eq!(deps[1].package, "text");
2048        assert_eq!(deps[2].package, "aeson");
2049    }
2050
2051    #[test]
2052    fn parse_deps_empty() {
2053        let deps = parse_dependencies_from_text("", NodeId(0));
2054        assert!(deps.is_empty());
2055    }
2056
2057    // -- Condition parsing tests ----------------------------------------------
2058
2059    #[test]
2060    fn parse_condition_flag() {
2061        let c = parse_condition("flag(dev)");
2062        assert_eq!(c, Condition::Flag("dev"));
2063    }
2064
2065    #[test]
2066    fn parse_condition_os() {
2067        let c = parse_condition("os(windows)");
2068        assert_eq!(c, Condition::OS("windows"));
2069    }
2070
2071    #[test]
2072    fn parse_condition_arch() {
2073        let c = parse_condition("arch(x86_64)");
2074        assert_eq!(c, Condition::Arch("x86_64"));
2075    }
2076
2077    #[test]
2078    fn parse_condition_impl() {
2079        let c = parse_condition("impl(ghc >= 9.6)");
2080        assert_eq!(
2081            c,
2082            Condition::Impl(
2083                "ghc",
2084                Some(VersionRange::Gte(Version {
2085                    components: vec![9, 6]
2086                }))
2087            )
2088        );
2089    }
2090
2091    #[test]
2092    fn parse_condition_not() {
2093        let c = parse_condition("!os(windows)");
2094        assert_eq!(c, Condition::Not(Box::new(Condition::OS("windows"))));
2095    }
2096
2097    #[test]
2098    fn parse_condition_and() {
2099        let c = parse_condition("flag(dev) && !os(windows)");
2100        assert_eq!(
2101            c,
2102            Condition::And(
2103                Box::new(Condition::Flag("dev")),
2104                Box::new(Condition::Not(Box::new(Condition::OS("windows")))),
2105            )
2106        );
2107    }
2108
2109    #[test]
2110    fn parse_condition_or() {
2111        let c = parse_condition("flag(a) || flag(b)");
2112        assert_eq!(
2113            c,
2114            Condition::Or(
2115                Box::new(Condition::Flag("a")),
2116                Box::new(Condition::Flag("b")),
2117            )
2118        );
2119    }
2120
2121    #[test]
2122    fn parse_condition_empty() {
2123        let c = parse_condition("");
2124        assert_eq!(c, Condition::Raw(""));
2125    }
2126
2127    // -- Full AST derivation tests --------------------------------------------
2128
2129    #[test]
2130    fn derive_minimal_file() {
2131        let src = "cabal-version: 3.0\nname: my-pkg\nversion: 0.1.0.0\n";
2132        let result = do_parse(src);
2133        let ast = derive_ast(&result.cst);
2134
2135        assert_eq!(ast.name, Some("my-pkg"));
2136        assert_eq!(
2137            ast.version,
2138            Some(Version {
2139                components: vec![0, 1, 0, 0]
2140            })
2141        );
2142        assert!(ast.cabal_version.is_some());
2143        let cv = ast.cabal_version.as_ref().unwrap();
2144        assert_eq!(cv.raw, "3.0");
2145        assert_eq!(
2146            cv.version,
2147            Some(Version {
2148                components: vec![3, 0]
2149            })
2150        );
2151    }
2152
2153    #[test]
2154    fn derive_with_library() {
2155        let src = "\
2156cabal-version: 3.0
2157name: my-pkg
2158version: 0.1.0.0
2159
2160library
2161  exposed-modules:
2162    Foo
2163    Bar
2164  build-depends:
2165    base >=4.14
2166  default-language: GHC2021
2167";
2168        let result = do_parse(src);
2169        let ast = derive_ast(&result.cst);
2170
2171        assert!(ast.library.is_some());
2172        let lib = ast.library.as_ref().unwrap();
2173        assert_eq!(lib.exposed_modules, vec!["Foo", "Bar"]);
2174        assert_eq!(lib.fields.build_depends.len(), 1);
2175        assert_eq!(lib.fields.build_depends[0].package, "base");
2176        assert_eq!(lib.fields.default_language, Some("GHC2021"));
2177    }
2178
2179    #[test]
2180    fn derive_with_executable() {
2181        let src = "\
2182cabal-version: 3.0
2183name: my-pkg
2184version: 0.1.0.0
2185
2186executable my-exe
2187  main-is: Main.hs
2188  build-depends: base
2189  hs-source-dirs: app
2190";
2191        let result = do_parse(src);
2192        let ast = derive_ast(&result.cst);
2193
2194        assert_eq!(ast.executables.len(), 1);
2195        let exe = &ast.executables[0];
2196        assert_eq!(exe.fields.name, Some("my-exe"));
2197        assert_eq!(exe.main_is, Some("Main.hs"));
2198        assert_eq!(exe.fields.build_depends.len(), 1);
2199        assert_eq!(exe.fields.hs_source_dirs, vec!["app"]);
2200    }
2201
2202    #[test]
2203    fn derive_with_test_suite() {
2204        let src = "\
2205cabal-version: 3.0
2206name: my-pkg
2207version: 0.1.0.0
2208
2209test-suite my-tests
2210  type: exitcode-stdio-1.0
2211  main-is: Main.hs
2212  build-depends: base, tasty
2213";
2214        let result = do_parse(src);
2215        let ast = derive_ast(&result.cst);
2216
2217        assert_eq!(ast.test_suites.len(), 1);
2218        let ts = &ast.test_suites[0];
2219        assert_eq!(ts.fields.name, Some("my-tests"));
2220        assert_eq!(ts.test_type, Some("exitcode-stdio-1.0"));
2221        assert_eq!(ts.main_is, Some("Main.hs"));
2222        assert_eq!(ts.fields.build_depends.len(), 2);
2223    }
2224
2225    #[test]
2226    fn derive_with_common_stanza() {
2227        let src = "\
2228cabal-version: 3.0
2229name: my-pkg
2230version: 0.1.0.0
2231
2232common warnings
2233  ghc-options: -Wall -Wcompat
2234
2235library
2236  import: warnings
2237  exposed-modules: Foo
2238";
2239        let result = do_parse(src);
2240        let ast = derive_ast(&result.cst);
2241
2242        assert_eq!(ast.common_stanzas.len(), 1);
2243        assert_eq!(ast.common_stanzas[0].name, "warnings");
2244        assert_eq!(
2245            ast.common_stanzas[0].fields.ghc_options,
2246            vec!["-Wall", "-Wcompat"]
2247        );
2248
2249        let lib = ast.library.as_ref().unwrap();
2250        assert_eq!(lib.fields.imports, vec!["warnings"]);
2251    }
2252
2253    #[test]
2254    fn derive_with_flag() {
2255        let src = "\
2256cabal-version: 3.0
2257name: my-pkg
2258version: 0.1.0.0
2259
2260flag dev
2261  description: Development mode
2262  default: False
2263  manual: True
2264";
2265        let result = do_parse(src);
2266        let ast = derive_ast(&result.cst);
2267
2268        assert_eq!(ast.flags.len(), 1);
2269        let flag = &ast.flags[0];
2270        assert_eq!(flag.name, "dev");
2271        assert_eq!(flag.description, Some("Development mode"));
2272        assert_eq!(flag.default, Some(false));
2273        assert_eq!(flag.manual, Some(true));
2274    }
2275
2276    #[test]
2277    fn derive_with_source_repository() {
2278        let src = "\
2279cabal-version: 3.0
2280name: my-pkg
2281version: 0.1.0.0
2282
2283source-repository head
2284  type: git
2285  location: https://github.com/example/my-pkg
2286";
2287        let result = do_parse(src);
2288        let ast = derive_ast(&result.cst);
2289
2290        assert_eq!(ast.source_repositories.len(), 1);
2291        let sr = &ast.source_repositories[0];
2292        assert_eq!(sr.kind, Some("head"));
2293        assert_eq!(sr.repo_type, Some("git"));
2294        assert_eq!(sr.location, Some("https://github.com/example/my-pkg"));
2295    }
2296
2297    #[test]
2298    fn derive_conditional() {
2299        let src = "\
2300cabal-version: 3.0
2301name: my-pkg
2302version: 0.1.0.0
2303
2304library
2305  build-depends: base
2306  if flag(dev)
2307    ghc-options: -O0
2308  else
2309    ghc-options: -O2
2310";
2311        let result = do_parse(src);
2312        let ast = derive_ast(&result.cst);
2313
2314        let lib = ast.library.as_ref().unwrap();
2315        assert_eq!(lib.fields.conditionals.len(), 1);
2316        let cond = &lib.fields.conditionals[0];
2317        assert_eq!(cond.condition, Condition::Flag("dev"));
2318        assert_eq!(cond.then_fields.len(), 1);
2319        assert_eq!(cond.then_fields[0].name, "ghc-options");
2320        assert_eq!(cond.then_fields[0].value, "-O0");
2321        assert_eq!(cond.else_fields.len(), 1);
2322        assert_eq!(cond.else_fields[0].name, "ghc-options");
2323        assert_eq!(cond.else_fields[0].value, "-O2");
2324    }
2325
2326    #[test]
2327    fn derive_all_dependencies() {
2328        let src = "\
2329cabal-version: 3.0
2330name: my-pkg
2331version: 0.1.0.0
2332
2333library
2334  build-depends: base, text
2335
2336executable my-exe
2337  build-depends: base, my-pkg
2338";
2339        let result = do_parse(src);
2340        let ast = derive_ast(&result.cst);
2341
2342        let all_deps = ast.all_dependencies();
2343        assert_eq!(all_deps.len(), 4);
2344        let names: Vec<&str> = all_deps.iter().map(|d| d.package).collect();
2345        assert!(names.contains(&"base"));
2346        assert!(names.contains(&"text"));
2347        assert!(names.contains(&"my-pkg"));
2348    }
2349
2350    #[test]
2351    fn derive_all_components() {
2352        let src = "\
2353cabal-version: 3.0
2354name: my-pkg
2355version: 0.1.0.0
2356
2357library
2358  exposed-modules: Foo
2359
2360executable my-exe
2361  main-is: Main.hs
2362
2363test-suite my-tests
2364  type: exitcode-stdio-1.0
2365  main-is: Main.hs
2366
2367benchmark my-bench
2368  type: exitcode-stdio-1.0
2369  main-is: Main.hs
2370";
2371        let result = do_parse(src);
2372        let ast = derive_ast(&result.cst);
2373
2374        let comps = ast.all_components();
2375        assert_eq!(comps.len(), 4);
2376    }
2377
2378    #[test]
2379    fn derive_find_component() {
2380        let src = "\
2381cabal-version: 3.0
2382name: my-pkg
2383version: 0.1.0.0
2384
2385library
2386  exposed-modules: Foo
2387
2388executable my-exe
2389  main-is: Main.hs
2390";
2391        let result = do_parse(src);
2392        let ast = derive_ast(&result.cst);
2393
2394        assert!(ast.find_component("library").is_some());
2395        assert!(ast.find_component("my-exe").is_some());
2396        assert!(ast.find_component("nonexistent").is_none());
2397    }
2398
2399    #[test]
2400    fn derive_cst_node_back_references_valid() {
2401        let src = "\
2402cabal-version: 3.0
2403name: my-pkg
2404version: 0.1.0.0
2405
2406library
2407  build-depends: base >=4.14
2408";
2409        let result = do_parse(src);
2410        let ast = derive_ast(&result.cst);
2411
2412        // The CST root back-reference should be valid.
2413        assert_eq!(ast.cst_root, result.cst.root);
2414
2415        // Library's cst_node should be a valid Section node.
2416        let lib = ast.library.as_ref().unwrap();
2417        let node = result.cst.node(lib.fields.cst_node);
2418        assert_eq!(node.kind, CstNodeKind::Section);
2419
2420        // Dependency's cst_node should be valid.
2421        assert!(!lib.fields.build_depends.is_empty());
2422        let dep_node_id = lib.fields.build_depends[0].cst_node;
2423        assert!(dep_node_id.0 < result.cst.node_count());
2424    }
2425
2426    #[test]
2427    fn derive_deps_leading_comma_style() {
2428        let src = "\
2429cabal-version: 3.0
2430name: my-pkg
2431version: 0.1.0.0
2432
2433library
2434  build-depends:
2435      base >=4.14
2436    , text >=2.0
2437    , aeson ^>=2.2
2438";
2439        let result = do_parse(src);
2440        let ast = derive_ast(&result.cst);
2441
2442        let lib = ast.library.as_ref().unwrap();
2443        assert_eq!(lib.fields.build_depends.len(), 3);
2444        assert_eq!(lib.fields.build_depends[0].package, "base");
2445        assert_eq!(lib.fields.build_depends[1].package, "text");
2446        assert_eq!(lib.fields.build_depends[2].package, "aeson");
2447    }
2448
2449    #[test]
2450    fn derive_deps_trailing_comma_style() {
2451        let src = "\
2452cabal-version: 3.0
2453name: my-pkg
2454version: 0.1.0.0
2455
2456library
2457  build-depends:
2458    base >=4.14,
2459    text >=2.0,
2460    aeson ^>=2.2
2461";
2462        let result = do_parse(src);
2463        let ast = derive_ast(&result.cst);
2464
2465        let lib = ast.library.as_ref().unwrap();
2466        assert_eq!(lib.fields.build_depends.len(), 3);
2467        assert_eq!(lib.fields.build_depends[0].package, "base");
2468        assert_eq!(lib.fields.build_depends[1].package, "text");
2469        assert_eq!(lib.fields.build_depends[2].package, "aeson");
2470    }
2471
2472    #[test]
2473    fn derive_deps_single_line() {
2474        let src = "\
2475cabal-version: 3.0
2476name: my-pkg
2477version: 0.1.0.0
2478
2479library
2480  build-depends: base >=4.14, text >=2.0, aeson ^>=2.2
2481";
2482        let result = do_parse(src);
2483        let ast = derive_ast(&result.cst);
2484
2485        let lib = ast.library.as_ref().unwrap();
2486        assert_eq!(lib.fields.build_depends.len(), 3);
2487    }
2488
2489    #[test]
2490    fn derive_default_extensions() {
2491        let src = "\
2492cabal-version: 3.0
2493name: my-pkg
2494version: 0.1.0.0
2495
2496library
2497  default-extensions:
2498    OverloadedStrings
2499    DerivingStrategies
2500";
2501        let result = do_parse(src);
2502        let ast = derive_ast(&result.cst);
2503
2504        let lib = ast.library.as_ref().unwrap();
2505        assert_eq!(
2506            lib.fields.default_extensions,
2507            vec!["OverloadedStrings", "DerivingStrategies"]
2508        );
2509    }
2510
2511    #[test]
2512    fn derive_metadata_fields() {
2513        let src = "\
2514cabal-version: 3.0
2515name: my-pkg
2516version: 0.1.0.0
2517license: MIT
2518synopsis: A test package
2519author: Test Author
2520maintainer: test@example.com
2521homepage: https://example.com
2522bug-reports: https://example.com/issues
2523category: Development
2524build-type: Simple
2525";
2526        let result = do_parse(src);
2527        let ast = derive_ast(&result.cst);
2528
2529        assert_eq!(ast.license, Some("MIT"));
2530        assert_eq!(ast.synopsis, Some("A test package"));
2531        assert_eq!(ast.author, Some("Test Author"));
2532        assert_eq!(ast.maintainer, Some("test@example.com"));
2533        assert_eq!(ast.homepage, Some("https://example.com"));
2534        assert_eq!(ast.bug_reports, Some("https://example.com/issues"));
2535        assert_eq!(ast.category, Some("Development"));
2536        assert_eq!(ast.build_type, Some("Simple"));
2537    }
2538
2539    #[test]
2540    fn derive_conditional_deps() {
2541        let src = "\
2542cabal-version: 3.0
2543name: my-pkg
2544version: 0.1.0.0
2545
2546library
2547  build-depends: base
2548  if os(windows)
2549    build-depends: Win32
2550  else
2551    build-depends: unix
2552";
2553        let result = do_parse(src);
2554        let ast = derive_ast(&result.cst);
2555
2556        let all_deps = ast.all_dependencies();
2557        let names: Vec<&str> = all_deps.iter().map(|d| d.package).collect();
2558        assert!(names.contains(&"base"));
2559        assert!(names.contains(&"Win32"));
2560        assert!(names.contains(&"unix"));
2561        assert_eq!(all_deps.len(), 3);
2562    }
2563
2564    // -- Boolean literal condition tests ----------------------------------------
2565
2566    #[test]
2567    fn parse_condition_true() {
2568        assert_eq!(parse_condition("true"), Condition::Lit(true));
2569    }
2570
2571    #[test]
2572    fn parse_condition_false() {
2573        assert_eq!(parse_condition("false"), Condition::Lit(false));
2574    }
2575
2576    #[test]
2577    fn parse_condition_true_case_insensitive() {
2578        assert_eq!(parse_condition("True"), Condition::Lit(true));
2579        assert_eq!(parse_condition("FALSE"), Condition::Lit(false));
2580    }
2581
2582    // -- Wildcard version range tests -------------------------------------------
2583
2584    #[test]
2585    fn version_range_wildcard() {
2586        let r = parse_version_range("==1.2.*").unwrap();
2587        match r {
2588            VersionRange::And(a, b) => {
2589                assert_eq!(
2590                    *a,
2591                    VersionRange::Gte(Version {
2592                        components: vec![1, 2]
2593                    })
2594                );
2595                assert_eq!(
2596                    *b,
2597                    VersionRange::Lt(Version {
2598                        components: vec![1, 3]
2599                    })
2600                );
2601            }
2602            _ => panic!("Expected And range, got {:?}", r),
2603        }
2604    }
2605
2606    // -- -any and -none version range tests -------------------------------------
2607
2608    #[test]
2609    fn version_range_any_keyword() {
2610        assert_eq!(parse_version_range("-any").unwrap(), VersionRange::Any);
2611    }
2612
2613    #[test]
2614    fn version_range_none_keyword() {
2615        assert_eq!(
2616            parse_version_range("-none").unwrap(),
2617            VersionRange::NoVersion
2618        );
2619    }
2620
2621    // -- Set notation version range tests ---------------------------------------
2622
2623    #[test]
2624    fn version_range_set_major_bound() {
2625        let r = parse_version_range("^>= { 2.6, 2.7, 2.8 }").unwrap();
2626        match r {
2627            VersionRange::Or(_, _) => {} // just verify it parses as Or
2628            _ => panic!("Expected Or range for set notation, got {:?}", r),
2629        }
2630    }
2631
2632    #[test]
2633    fn version_range_set_eq() {
2634        let r = parse_version_range("== { 1.0, 2.0 }").unwrap();
2635        match r {
2636            VersionRange::Or(_, _) => {}
2637            _ => panic!("Expected Or range for set notation, got {:?}", r),
2638        }
2639    }
2640
2641    // -- Display tests for new variants -----------------------------------------
2642
2643    #[test]
2644    fn version_range_display_any() {
2645        assert_eq!(VersionRange::Any.to_string(), "-any");
2646    }
2647
2648    #[test]
2649    fn version_range_display_none() {
2650        assert_eq!(VersionRange::NoVersion.to_string(), "-none");
2651    }
2652
2653    #[test]
2654    fn derive_benchmark() {
2655        let src = "\
2656cabal-version: 3.0
2657name: my-pkg
2658version: 0.1.0.0
2659
2660benchmark my-bench
2661  type: exitcode-stdio-1.0
2662  main-is: Main.hs
2663  build-depends: base, criterion
2664  hs-source-dirs: bench
2665";
2666        let result = do_parse(src);
2667        let ast = derive_ast(&result.cst);
2668
2669        assert_eq!(ast.benchmarks.len(), 1);
2670        let bm = &ast.benchmarks[0];
2671        assert_eq!(bm.fields.name, Some("my-bench"));
2672        assert_eq!(bm.bench_type, Some("exitcode-stdio-1.0"));
2673        assert_eq!(bm.main_is, Some("Main.hs"));
2674        assert_eq!(bm.fields.build_depends.len(), 2);
2675    }
2676}