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 => {
1056            // Only process as top-level if parent is Root.
1057            if cst.node(node_id).parent == Some(cst.root) {
1058                derive_top_level_field(cst, node_id, file);
1059            }
1060        }
1061        CstNodeKind::Section => {
1062            // Check if this is a known section type (library, executable, etc.).
1063            let source = cst.source.as_str();
1064            let is_top_level_section = if let Some(ref kw_span) = node.section_keyword {
1065                let kw = kw_span.slice(source).to_ascii_lowercase();
1066                matches!(
1067                    kw.as_str(),
1068                    "library"
1069                        | "executable"
1070                        | "test-suite"
1071                        | "benchmark"
1072                        | "common"
1073                        | "flag"
1074                        | "source-repository"
1075                )
1076            } else {
1077                false
1078            };
1079
1080            if is_top_level_section {
1081                derive_section(cst, node_id, file);
1082                // Also recursively check this section's children for more
1083                // sections that may have been nested by the parser.
1084                let children: Vec<NodeId> = cst.node(node_id).children.clone();
1085                for child_id in children {
1086                    let child = cst.node(child_id);
1087                    if child.kind == CstNodeKind::Section {
1088                        collect_ast_nodes(cst, child_id, file);
1089                    }
1090                }
1091            }
1092        }
1093        _ => {}
1094    }
1095}
1096
1097/// Extract the full value text for a field node, including continuation
1098/// (ValueLine) children. The result is trimmed.
1099fn field_full_value(cst: &CabalCst, node_id: NodeId) -> String {
1100    let node = cst.node(node_id);
1101    let source = cst.source.as_str();
1102
1103    let mut value = String::new();
1104
1105    // First line value.
1106    if let Some(ref val_span) = node.field_value {
1107        value.push_str(val_span.slice(source).trim());
1108    }
1109
1110    // Continuation lines.
1111    for &child_id in &node.children {
1112        let child = cst.node(child_id);
1113        if child.kind == CstNodeKind::ValueLine {
1114            let line_text = child.content_span.slice(source).trim();
1115            if !line_text.is_empty() {
1116                if !value.is_empty() {
1117                    value.push('\n');
1118                }
1119                value.push_str(line_text);
1120            }
1121        }
1122    }
1123
1124    value
1125}
1126
1127/// Get the field value as a borrowed str reference (trimmed).
1128/// Checks the first line, then falls back to the first continuation line.
1129/// Handles fields like `name:\n  hedgehog`.
1130fn field_first_line_value(cst: &CabalCst, node_id: NodeId) -> Option<&str> {
1131    let node = cst.node(node_id);
1132    let source = cst.source.as_str();
1133
1134    // Try the first-line value.
1135    if let Some(ref val_span) = node.field_value {
1136        let v = val_span.slice(source).trim();
1137        if !v.is_empty() {
1138            return Some(v);
1139        }
1140    }
1141
1142    // Fall back to first non-empty continuation line.
1143    for &child_id in &node.children {
1144        let child = cst.node(child_id);
1145        if child.kind == CstNodeKind::ValueLine {
1146            let v = child.content_span.slice(source).trim();
1147            if !v.is_empty() {
1148                return Some(v);
1149            }
1150        }
1151    }
1152
1153    None
1154}
1155
1156/// Parse a whitespace/newline-separated list from a field value.
1157/// Used for `exposed-modules`, `other-modules`, `hs-source-dirs`,
1158/// `default-extensions`, etc.
1159fn parse_list_field(cst: &CabalCst, node_id: NodeId) -> Vec<&str> {
1160    let node = cst.node(node_id);
1161    let source = cst.source.as_str();
1162    let mut items = Vec::new();
1163
1164    // First line.
1165    if let Some(ref val_span) = node.field_value {
1166        let text = val_span.slice(source).trim();
1167        for item in split_list_items(text) {
1168            if !item.is_empty() {
1169                items.push(item);
1170            }
1171        }
1172    }
1173
1174    // Continuation lines.
1175    for &child_id in &node.children {
1176        let child = cst.node(child_id);
1177        if child.kind == CstNodeKind::ValueLine {
1178            let text = child.content_span.slice(source).trim();
1179            for item in split_list_items(text) {
1180                if !item.is_empty() {
1181                    items.push(item);
1182                }
1183            }
1184        }
1185    }
1186
1187    items
1188}
1189
1190/// Split a line into list items, handling commas as separators and stripping
1191/// leading/trailing commas.
1192fn split_list_items(text: &str) -> Vec<&str> {
1193    let mut items = Vec::new();
1194    if text.contains(',') {
1195        for part in text.split(',') {
1196            let trimmed = part.trim();
1197            if !trimmed.is_empty() {
1198                items.push(trimmed);
1199            }
1200        }
1201    } else {
1202        // Space-separated.
1203        for part in text.split_whitespace() {
1204            items.push(part);
1205        }
1206    }
1207    items
1208}
1209
1210/// Parse `ghc-options` value: space-separated tokens, possibly multi-line.
1211fn parse_ghc_options(cst: &CabalCst, node_id: NodeId) -> Vec<&str> {
1212    let node = cst.node(node_id);
1213    let source = cst.source.as_str();
1214    let mut opts = Vec::new();
1215
1216    if let Some(ref val_span) = node.field_value {
1217        for opt in val_span.slice(source).split_whitespace() {
1218            opts.push(opt);
1219        }
1220    }
1221
1222    for &child_id in &node.children {
1223        let child = cst.node(child_id);
1224        if child.kind == CstNodeKind::ValueLine {
1225            for opt in child.content_span.slice(source).split_whitespace() {
1226                opts.push(opt);
1227            }
1228        }
1229    }
1230
1231    opts
1232}
1233
1234/// Parse dependencies from a `build-depends` field node.
1235fn parse_build_depends<'a>(cst: &'a CabalCst, node_id: NodeId) -> Vec<Dependency<'a>> {
1236    let node = cst.node(node_id);
1237    let source = cst.source.as_str();
1238    let mut deps = Vec::new();
1239
1240    // First line value.
1241    if let Some(ref val_span) = node.field_value {
1242        let text = val_span.slice(source).trim();
1243        deps.extend(parse_dependencies_from_text(text, node_id));
1244    }
1245
1246    // Continuation lines.
1247    for &child_id in &node.children {
1248        let child = cst.node(child_id);
1249        if child.kind == CstNodeKind::ValueLine {
1250            let text = child.content_span.slice(source).trim();
1251            if !text.is_empty() {
1252                // Use the child's NodeId so back-references point to the
1253                // specific ValueLine.
1254                deps.extend(parse_dependencies_from_text(text, child_id));
1255            }
1256        }
1257    }
1258
1259    deps
1260}
1261
1262/// Derive a top-level field into the CabalFile metadata.
1263fn derive_top_level_field<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
1264    let node = cst.node(node_id);
1265    let source = cst.source.as_str();
1266
1267    let raw_name = match node.field_name {
1268        Some(ref span) => span.slice(source),
1269        None => return,
1270    };
1271    let canon = canonicalize_field_name(raw_name);
1272
1273    match canon.as_str() {
1274        "cabal-version" => {
1275            let raw = field_first_line_value(cst, node_id).unwrap_or("");
1276            // Strip leading `>=` that some old files use.
1277            let version_str = raw.strip_prefix(">=").unwrap_or(raw).trim();
1278            file.cabal_version = Some(CabalVersion {
1279                raw,
1280                version: Version::parse(version_str),
1281                cst_node: node_id,
1282            });
1283        }
1284        "name" => {
1285            file.name = field_first_line_value(cst, node_id);
1286        }
1287        "version" => {
1288            let raw = field_first_line_value(cst, node_id).unwrap_or("");
1289            file.version = Version::parse(raw);
1290        }
1291        "license" => {
1292            file.license = field_first_line_value(cst, node_id);
1293        }
1294        "synopsis" => {
1295            file.synopsis = field_first_line_value(cst, node_id);
1296        }
1297        "description" => {
1298            // Description can be multi-line; we store a reference to the first
1299            // line and let callers use `field_full_value` if they need all of
1300            // it. For the AST we just capture the first line.
1301            file.description = field_first_line_value(cst, node_id);
1302        }
1303        "author" => {
1304            file.author = field_first_line_value(cst, node_id);
1305        }
1306        "maintainer" => {
1307            file.maintainer = field_first_line_value(cst, node_id);
1308        }
1309        "homepage" => {
1310            file.homepage = field_first_line_value(cst, node_id);
1311        }
1312        "bug-reports" => {
1313            file.bug_reports = field_first_line_value(cst, node_id);
1314        }
1315        "category" => {
1316            file.category = field_first_line_value(cst, node_id);
1317        }
1318        "build-type" => {
1319            file.build_type = field_first_line_value(cst, node_id);
1320        }
1321        "tested-with" => {
1322            file.tested_with = field_first_line_value(cst, node_id);
1323        }
1324        "extra-source-files" | "extra-doc-files" => {
1325            file.extra_source_files
1326                .extend(parse_list_field(cst, node_id));
1327        }
1328        _ => {
1329            let value = field_full_value(cst, node_id);
1330            file.other_fields.push(Field {
1331                name: canon,
1332                raw_name,
1333                value,
1334                cst_node: node_id,
1335            });
1336        }
1337    }
1338}
1339
1340/// Derive a section (library, executable, etc.) into the CabalFile.
1341fn derive_section<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
1342    let node = cst.node(node_id);
1343    let source = cst.source.as_str();
1344
1345    let keyword = match node.section_keyword {
1346        Some(ref span) => span.slice(source),
1347        None => return,
1348    };
1349    let section_arg = node.section_arg.map(|span| span.slice(source));
1350    let keyword_lower = keyword.to_ascii_lowercase();
1351
1352    match keyword_lower.as_str() {
1353        "library" => {
1354            let lib = derive_library(cst, node_id, section_arg);
1355            if section_arg.is_some() {
1356                file.named_libraries.push(lib);
1357            } else {
1358                file.library = Some(lib);
1359            }
1360        }
1361        "executable" => {
1362            let exe = derive_executable(cst, node_id, section_arg);
1363            file.executables.push(exe);
1364        }
1365        "test-suite" => {
1366            let ts = derive_test_suite(cst, node_id, section_arg);
1367            file.test_suites.push(ts);
1368        }
1369        "benchmark" => {
1370            let bm = derive_benchmark(cst, node_id, section_arg);
1371            file.benchmarks.push(bm);
1372        }
1373        "common" => {
1374            if let Some(name) = section_arg {
1375                let cs = derive_common_stanza(cst, node_id, name);
1376                file.common_stanzas.push(cs);
1377            }
1378        }
1379        "flag" => {
1380            if let Some(name) = section_arg {
1381                let flag = derive_flag(cst, node_id, name);
1382                file.flags.push(flag);
1383            }
1384        }
1385        "source-repository" => {
1386            let sr = derive_source_repository(cst, node_id, section_arg);
1387            file.source_repositories.push(sr);
1388        }
1389        _ => {
1390            // Unknown section type — ignore for now.
1391        }
1392    }
1393}
1394
1395/// Create default empty `ComponentFields`.
1396fn empty_component_fields<'a>(name: Option<&'a str>, cst_node: NodeId) -> ComponentFields<'a> {
1397    ComponentFields {
1398        name,
1399        cst_node,
1400        imports: Vec::new(),
1401        build_depends: Vec::new(),
1402        other_modules: Vec::new(),
1403        hs_source_dirs: Vec::new(),
1404        default_language: None,
1405        default_extensions: Vec::new(),
1406        ghc_options: Vec::new(),
1407        other_fields: Vec::new(),
1408        conditionals: Vec::new(),
1409    }
1410}
1411
1412/// Populate `ComponentFields` from the children of a section node.
1413fn populate_component_fields<'a>(
1414    cst: &'a CabalCst,
1415    section_id: NodeId,
1416    fields: &mut ComponentFields<'a>,
1417) {
1418    let section = cst.node(section_id);
1419    let source = cst.source.as_str();
1420
1421    for &child_id in &section.children {
1422        let child = cst.node(child_id);
1423        match child.kind {
1424            CstNodeKind::Field => {
1425                let raw_name = match child.field_name {
1426                    Some(ref span) => span.slice(source),
1427                    None => continue,
1428                };
1429                let canon = canonicalize_field_name(raw_name);
1430
1431                match canon.as_str() {
1432                    "build-depends" => {
1433                        fields
1434                            .build_depends
1435                            .extend(parse_build_depends(cst, child_id));
1436                    }
1437                    "exposed-modules" => {
1438                        // Handled by caller if Library.
1439                        // We still parse here and caller picks it up.
1440                    }
1441                    "other-modules" => {
1442                        fields.other_modules.extend(parse_list_field(cst, child_id));
1443                    }
1444                    "hs-source-dirs" => {
1445                        fields
1446                            .hs_source_dirs
1447                            .extend(parse_list_field(cst, child_id));
1448                    }
1449                    "default-language" => {
1450                        fields.default_language = field_first_line_value(cst, child_id);
1451                    }
1452                    "default-extensions" | "extensions" => {
1453                        fields
1454                            .default_extensions
1455                            .extend(parse_list_field(cst, child_id));
1456                    }
1457                    "ghc-options" => {
1458                        fields.ghc_options.extend(parse_ghc_options(cst, child_id));
1459                    }
1460                    _ => {
1461                        let value = field_full_value(cst, child_id);
1462                        fields.other_fields.push(Field {
1463                            name: canon,
1464                            raw_name,
1465                            value,
1466                            cst_node: child_id,
1467                        });
1468                    }
1469                }
1470            }
1471            CstNodeKind::Import => {
1472                if let Some(ref val_span) = child.field_value {
1473                    let val = val_span.slice(source).trim();
1474                    if !val.is_empty() {
1475                        // imports can be comma-separated
1476                        for item in val.split(',') {
1477                            let item = item.trim();
1478                            if !item.is_empty() {
1479                                fields.imports.push(item);
1480                            }
1481                        }
1482                    }
1483                }
1484            }
1485            CstNodeKind::Conditional => {
1486                let cond = derive_conditional(cst, child_id);
1487                fields.conditionals.push(cond);
1488            }
1489            // Comments, blank lines, value lines — skip for AST.
1490            _ => {}
1491        }
1492    }
1493}
1494
1495/// Derive a conditional block from a CST Conditional node.
1496fn derive_conditional<'a>(cst: &'a CabalCst, node_id: NodeId) -> Conditional<'a> {
1497    let node = cst.node(node_id);
1498    let source = cst.source.as_str();
1499
1500    // Parse condition expression.
1501    let condition = match node.condition_expr {
1502        Some(ref span) => parse_condition(span.slice(source)),
1503        None => Condition::Raw(""),
1504    };
1505
1506    let mut cond = Conditional {
1507        condition,
1508        then_fields: Vec::new(),
1509        then_deps: Vec::new(),
1510        else_fields: Vec::new(),
1511        else_deps: Vec::new(),
1512        then_conditionals: Vec::new(),
1513        else_conditionals: Vec::new(),
1514        cst_node: node_id,
1515    };
1516
1517    // Process children: then-block items, then the ElseBlock.
1518    for &child_id in &node.children {
1519        let child = cst.node(child_id);
1520        match child.kind {
1521            CstNodeKind::Field => {
1522                let raw_name = match child.field_name {
1523                    Some(ref span) => span.slice(source),
1524                    None => continue,
1525                };
1526                let canon = canonicalize_field_name(raw_name);
1527
1528                if canon == "build-depends" {
1529                    cond.then_deps.extend(parse_build_depends(cst, child_id));
1530                } else {
1531                    let value = field_full_value(cst, child_id);
1532                    cond.then_fields.push(Field {
1533                        name: canon,
1534                        raw_name,
1535                        value,
1536                        cst_node: child_id,
1537                    });
1538                }
1539            }
1540            CstNodeKind::Conditional => {
1541                cond.then_conditionals
1542                    .push(derive_conditional(cst, child_id));
1543            }
1544            CstNodeKind::ElseBlock => {
1545                // Process else block children.
1546                for &else_child_id in &child.children {
1547                    let else_child = cst.node(else_child_id);
1548                    match else_child.kind {
1549                        CstNodeKind::Field => {
1550                            let raw_name = match else_child.field_name {
1551                                Some(ref span) => span.slice(source),
1552                                None => continue,
1553                            };
1554                            let canon = canonicalize_field_name(raw_name);
1555
1556                            if canon == "build-depends" {
1557                                cond.else_deps
1558                                    .extend(parse_build_depends(cst, else_child_id));
1559                            } else {
1560                                let value = field_full_value(cst, else_child_id);
1561                                cond.else_fields.push(Field {
1562                                    name: canon,
1563                                    raw_name,
1564                                    value,
1565                                    cst_node: else_child_id,
1566                                });
1567                            }
1568                        }
1569                        CstNodeKind::Conditional => {
1570                            cond.else_conditionals
1571                                .push(derive_conditional(cst, else_child_id));
1572                        }
1573                        _ => {}
1574                    }
1575                }
1576            }
1577            _ => {}
1578        }
1579    }
1580
1581    cond
1582}
1583
1584/// Derive a Library from a CST section node.
1585fn derive_library<'a>(cst: &'a CabalCst, node_id: NodeId, name: Option<&'a str>) -> Library<'a> {
1586    let mut fields = empty_component_fields(name, node_id);
1587    populate_component_fields(cst, node_id, &mut fields);
1588
1589    // Extract exposed-modules from section children (since populate_component_fields
1590    // skips it for the generic path).
1591    let exposed_modules = extract_exposed_modules(cst, node_id);
1592
1593    Library {
1594        fields,
1595        exposed_modules,
1596    }
1597}
1598
1599/// Extract `exposed-modules` from a section's children.
1600fn extract_exposed_modules(cst: &CabalCst, section_id: NodeId) -> Vec<&str> {
1601    let section = cst.node(section_id);
1602    let source = cst.source.as_str();
1603    let mut modules = Vec::new();
1604
1605    for &child_id in &section.children {
1606        let child = cst.node(child_id);
1607        if child.kind == CstNodeKind::Field {
1608            if let Some(ref name_span) = child.field_name {
1609                let canon = canonicalize_field_name(name_span.slice(source));
1610                if canon == "exposed-modules" {
1611                    modules.extend(parse_list_field(cst, child_id));
1612                }
1613            }
1614        }
1615    }
1616
1617    modules
1618}
1619
1620/// Derive an Executable from a CST section node.
1621fn derive_executable<'a>(
1622    cst: &'a CabalCst,
1623    node_id: NodeId,
1624    name: Option<&'a str>,
1625) -> Executable<'a> {
1626    let main_is = find_field_value_in_section(cst, node_id, "main-is");
1627
1628    let mut fields = empty_component_fields(name, node_id);
1629    populate_component_fields(cst, node_id, &mut fields);
1630    remove_field_by_name(&mut fields.other_fields, "main-is");
1631
1632    Executable { fields, main_is }
1633}
1634
1635/// Derive a TestSuite from a CST section node.
1636fn derive_test_suite<'a>(
1637    cst: &'a CabalCst,
1638    node_id: NodeId,
1639    name: Option<&'a str>,
1640) -> TestSuite<'a> {
1641    let test_type = find_field_value_in_section(cst, node_id, "type");
1642    let main_is = find_field_value_in_section(cst, node_id, "main-is");
1643
1644    let mut fields = empty_component_fields(name, node_id);
1645    populate_component_fields(cst, node_id, &mut fields);
1646    remove_field_by_name(&mut fields.other_fields, "type");
1647    remove_field_by_name(&mut fields.other_fields, "main-is");
1648
1649    TestSuite {
1650        fields,
1651        test_type,
1652        main_is,
1653    }
1654}
1655
1656/// Derive a Benchmark from a CST section node.
1657fn derive_benchmark<'a>(
1658    cst: &'a CabalCst,
1659    node_id: NodeId,
1660    name: Option<&'a str>,
1661) -> Benchmark<'a> {
1662    let bench_type = find_field_value_in_section(cst, node_id, "type");
1663    let main_is = find_field_value_in_section(cst, node_id, "main-is");
1664
1665    let mut fields = empty_component_fields(name, node_id);
1666    populate_component_fields(cst, node_id, &mut fields);
1667    remove_field_by_name(&mut fields.other_fields, "type");
1668    remove_field_by_name(&mut fields.other_fields, "main-is");
1669
1670    Benchmark {
1671        fields,
1672        bench_type,
1673        main_is,
1674    }
1675}
1676
1677/// Derive a CommonStanza from a CST section node.
1678fn derive_common_stanza<'a>(cst: &'a CabalCst, node_id: NodeId, name: &'a str) -> CommonStanza<'a> {
1679    let mut fields = empty_component_fields(Some(name), node_id);
1680    populate_component_fields(cst, node_id, &mut fields);
1681
1682    CommonStanza { name, fields }
1683}
1684
1685/// Derive a Flag from a CST section node.
1686fn derive_flag<'a>(cst: &'a CabalCst, node_id: NodeId, name: &'a str) -> Flag<'a> {
1687    let section = cst.node(node_id);
1688    let source = cst.source.as_str();
1689
1690    let mut description = None;
1691    let mut default = None;
1692    let mut manual = None;
1693    let mut other_fields = Vec::new();
1694
1695    for &child_id in &section.children {
1696        let child = cst.node(child_id);
1697        if child.kind == CstNodeKind::Field {
1698            let raw_name = match child.field_name {
1699                Some(ref span) => span.slice(source),
1700                None => continue,
1701            };
1702            let canon = canonicalize_field_name(raw_name);
1703
1704            match canon.as_str() {
1705                "description" => {
1706                    description = field_first_line_value(cst, child_id);
1707                }
1708                "default" => {
1709                    if let Some(val) = field_first_line_value(cst, child_id) {
1710                        let lower = val.to_ascii_lowercase();
1711                        default = Some(lower == "true");
1712                    }
1713                }
1714                "manual" => {
1715                    if let Some(val) = field_first_line_value(cst, child_id) {
1716                        let lower = val.to_ascii_lowercase();
1717                        manual = Some(lower == "true");
1718                    }
1719                }
1720                _ => {
1721                    let value = field_full_value(cst, child_id);
1722                    other_fields.push(Field {
1723                        name: canon,
1724                        raw_name,
1725                        value,
1726                        cst_node: child_id,
1727                    });
1728                }
1729            }
1730        }
1731    }
1732
1733    Flag {
1734        name,
1735        description,
1736        default,
1737        manual,
1738        other_fields,
1739        cst_node: node_id,
1740    }
1741}
1742
1743/// Derive a SourceRepository from a CST section node.
1744fn derive_source_repository<'a>(
1745    cst: &'a CabalCst,
1746    node_id: NodeId,
1747    kind: Option<&'a str>,
1748) -> SourceRepository<'a> {
1749    let section = cst.node(node_id);
1750    let source = cst.source.as_str();
1751
1752    let mut repo_type = None;
1753    let mut location = None;
1754    let mut tag = None;
1755    let mut branch = None;
1756    let mut subdir = None;
1757    let mut other_fields = Vec::new();
1758
1759    for &child_id in &section.children {
1760        let child = cst.node(child_id);
1761        if child.kind == CstNodeKind::Field {
1762            let raw_name = match child.field_name {
1763                Some(ref span) => span.slice(source),
1764                None => continue,
1765            };
1766            let canon = canonicalize_field_name(raw_name);
1767
1768            match canon.as_str() {
1769                "type" => {
1770                    repo_type = field_first_line_value(cst, child_id);
1771                }
1772                "location" => {
1773                    location = field_first_line_value(cst, child_id);
1774                }
1775                "tag" => {
1776                    tag = field_first_line_value(cst, child_id);
1777                }
1778                "branch" => {
1779                    branch = field_first_line_value(cst, child_id);
1780                }
1781                "subdir" => {
1782                    subdir = field_first_line_value(cst, child_id);
1783                }
1784                _ => {
1785                    let value = field_full_value(cst, child_id);
1786                    other_fields.push(Field {
1787                        name: canon,
1788                        raw_name,
1789                        value,
1790                        cst_node: child_id,
1791                    });
1792                }
1793            }
1794        }
1795    }
1796
1797    SourceRepository {
1798        kind,
1799        repo_type,
1800        location,
1801        tag,
1802        branch,
1803        subdir,
1804        other_fields,
1805        cst_node: node_id,
1806    }
1807}
1808
1809/// Remove a field by canonicalized name from `other_fields`.
1810fn remove_field_by_name(fields: &mut Vec<Field<'_>>, canonical_name: &str) {
1811    fields.retain(|f| f.name != canonical_name);
1812}
1813
1814/// Look up a field by canonicalized name in a section's children and return
1815/// its first-line value.
1816fn find_field_value_in_section<'a>(
1817    cst: &'a CabalCst,
1818    section_id: NodeId,
1819    target_canon: &str,
1820) -> Option<&'a str> {
1821    let section = cst.node(section_id);
1822    let source = cst.source.as_str();
1823
1824    for &child_id in &section.children {
1825        let child = cst.node(child_id);
1826        if child.kind == CstNodeKind::Field {
1827            if let Some(ref name_span) = child.field_name {
1828                let canon = canonicalize_field_name(name_span.slice(source));
1829                if canon == target_canon {
1830                    return field_first_line_value(cst, child_id);
1831                }
1832            }
1833        }
1834    }
1835    None
1836}
1837
1838// ---------------------------------------------------------------------------
1839// Tests
1840// ---------------------------------------------------------------------------
1841
1842#[cfg(test)]
1843mod tests {
1844    use super::*;
1845
1846    /// Parse source and return the ParseResult. Callers derive the AST from it.
1847    fn do_parse(source: &str) -> crate::parse::ParseResult {
1848        crate::parse::parse(source)
1849    }
1850
1851    // -- Version parsing tests ------------------------------------------------
1852
1853    #[test]
1854    fn version_parse_simple() {
1855        let v = Version::parse("0.1.0.0").unwrap();
1856        assert_eq!(v.components, vec![0, 1, 0, 0]);
1857    }
1858
1859    #[test]
1860    fn version_parse_two_components() {
1861        let v = Version::parse("4.14").unwrap();
1862        assert_eq!(v.components, vec![4, 14]);
1863    }
1864
1865    #[test]
1866    fn version_parse_single() {
1867        let v = Version::parse("5").unwrap();
1868        assert_eq!(v.components, vec![5]);
1869    }
1870
1871    #[test]
1872    fn version_parse_empty() {
1873        assert!(Version::parse("").is_none());
1874    }
1875
1876    #[test]
1877    fn version_parse_invalid() {
1878        assert!(Version::parse("abc").is_none());
1879        assert!(Version::parse("1.2.abc").is_none());
1880    }
1881
1882    #[test]
1883    fn version_display() {
1884        let v = Version {
1885            components: vec![1, 2, 3, 0],
1886        };
1887        assert_eq!(v.to_string(), "1.2.3.0");
1888    }
1889
1890    // -- Version range parsing tests ------------------------------------------
1891
1892    #[test]
1893    fn version_range_gte() {
1894        let vr = parse_version_range(">=4.14").unwrap();
1895        assert_eq!(
1896            vr,
1897            VersionRange::Gte(Version {
1898                components: vec![4, 14]
1899            })
1900        );
1901    }
1902
1903    #[test]
1904    fn version_range_lt() {
1905        let vr = parse_version_range("<5").unwrap();
1906        assert_eq!(
1907            vr,
1908            VersionRange::Lt(Version {
1909                components: vec![5]
1910            })
1911        );
1912    }
1913
1914    #[test]
1915    fn version_range_major_bound() {
1916        let vr = parse_version_range("^>=2.2").unwrap();
1917        assert_eq!(
1918            vr,
1919            VersionRange::MajorBound(Version {
1920                components: vec![2, 2]
1921            })
1922        );
1923    }
1924
1925    #[test]
1926    fn version_range_eq() {
1927        let vr = parse_version_range("==1.0").unwrap();
1928        assert_eq!(
1929            vr,
1930            VersionRange::Eq(Version {
1931                components: vec![1, 0]
1932            })
1933        );
1934    }
1935
1936    #[test]
1937    fn version_range_and() {
1938        let vr = parse_version_range(">=4.14 && <5").unwrap();
1939        assert_eq!(
1940            vr,
1941            VersionRange::And(
1942                Box::new(VersionRange::Gte(Version {
1943                    components: vec![4, 14]
1944                })),
1945                Box::new(VersionRange::Lt(Version {
1946                    components: vec![5]
1947                })),
1948            )
1949        );
1950    }
1951
1952    #[test]
1953    fn version_range_or() {
1954        let vr = parse_version_range(">=2.0 || ==1.9").unwrap();
1955        assert_eq!(
1956            vr,
1957            VersionRange::Or(
1958                Box::new(VersionRange::Gte(Version {
1959                    components: vec![2, 0]
1960                })),
1961                Box::new(VersionRange::Eq(Version {
1962                    components: vec![1, 9]
1963                })),
1964            )
1965        );
1966    }
1967
1968    #[test]
1969    fn version_range_complex_and() {
1970        let vr = parse_version_range(">=2.0 && <2.2").unwrap();
1971        assert_eq!(
1972            vr,
1973            VersionRange::And(
1974                Box::new(VersionRange::Gte(Version {
1975                    components: vec![2, 0]
1976                })),
1977                Box::new(VersionRange::Lt(Version {
1978                    components: vec![2, 2]
1979                })),
1980            )
1981        );
1982    }
1983
1984    #[test]
1985    fn version_range_empty() {
1986        assert!(parse_version_range("").is_none());
1987    }
1988
1989    // -- Canonicalize field name tests ----------------------------------------
1990
1991    #[test]
1992    fn canonicalize_mixed_case() {
1993        assert_eq!(canonicalize_field_name("Build-Depends"), "build-depends");
1994    }
1995
1996    #[test]
1997    fn canonicalize_underscore() {
1998        assert_eq!(canonicalize_field_name("build_depends"), "build-depends");
1999    }
2000
2001    #[test]
2002    fn canonicalize_already_canonical() {
2003        assert_eq!(canonicalize_field_name("build-depends"), "build-depends");
2004    }
2005
2006    // -- Dependency parsing tests ---------------------------------------------
2007
2008    #[test]
2009    fn parse_dep_no_version() {
2010        let dep = parse_single_dependency("base", NodeId(0)).unwrap();
2011        assert_eq!(dep.package, "base");
2012        assert!(dep.version_range.is_none());
2013    }
2014
2015    #[test]
2016    fn parse_dep_with_version() {
2017        let dep = parse_single_dependency("aeson ^>=2.2", NodeId(0)).unwrap();
2018        assert_eq!(dep.package, "aeson");
2019        assert_eq!(
2020            dep.version_range,
2021            Some(VersionRange::MajorBound(Version {
2022                components: vec![2, 2]
2023            }))
2024        );
2025    }
2026
2027    #[test]
2028    fn parse_dep_with_range() {
2029        let dep = parse_single_dependency("base >=4.14 && <5", NodeId(0)).unwrap();
2030        assert_eq!(dep.package, "base");
2031        assert_eq!(
2032            dep.version_range,
2033            Some(VersionRange::And(
2034                Box::new(VersionRange::Gte(Version {
2035                    components: vec![4, 14]
2036                })),
2037                Box::new(VersionRange::Lt(Version {
2038                    components: vec![5]
2039                })),
2040            ))
2041        );
2042    }
2043
2044    #[test]
2045    fn parse_deps_comma_separated() {
2046        let deps = parse_dependencies_from_text("base >=4.14, text >=2.0, aeson ^>=2.2", NodeId(0));
2047        assert_eq!(deps.len(), 3);
2048        assert_eq!(deps[0].package, "base");
2049        assert_eq!(deps[1].package, "text");
2050        assert_eq!(deps[2].package, "aeson");
2051    }
2052
2053    #[test]
2054    fn parse_deps_empty() {
2055        let deps = parse_dependencies_from_text("", NodeId(0));
2056        assert!(deps.is_empty());
2057    }
2058
2059    // -- Condition parsing tests ----------------------------------------------
2060
2061    #[test]
2062    fn parse_condition_flag() {
2063        let c = parse_condition("flag(dev)");
2064        assert_eq!(c, Condition::Flag("dev"));
2065    }
2066
2067    #[test]
2068    fn parse_condition_os() {
2069        let c = parse_condition("os(windows)");
2070        assert_eq!(c, Condition::OS("windows"));
2071    }
2072
2073    #[test]
2074    fn parse_condition_arch() {
2075        let c = parse_condition("arch(x86_64)");
2076        assert_eq!(c, Condition::Arch("x86_64"));
2077    }
2078
2079    #[test]
2080    fn parse_condition_impl() {
2081        let c = parse_condition("impl(ghc >= 9.6)");
2082        assert_eq!(
2083            c,
2084            Condition::Impl(
2085                "ghc",
2086                Some(VersionRange::Gte(Version {
2087                    components: vec![9, 6]
2088                }))
2089            )
2090        );
2091    }
2092
2093    #[test]
2094    fn parse_condition_not() {
2095        let c = parse_condition("!os(windows)");
2096        assert_eq!(c, Condition::Not(Box::new(Condition::OS("windows"))));
2097    }
2098
2099    #[test]
2100    fn parse_condition_and() {
2101        let c = parse_condition("flag(dev) && !os(windows)");
2102        assert_eq!(
2103            c,
2104            Condition::And(
2105                Box::new(Condition::Flag("dev")),
2106                Box::new(Condition::Not(Box::new(Condition::OS("windows")))),
2107            )
2108        );
2109    }
2110
2111    #[test]
2112    fn parse_condition_or() {
2113        let c = parse_condition("flag(a) || flag(b)");
2114        assert_eq!(
2115            c,
2116            Condition::Or(
2117                Box::new(Condition::Flag("a")),
2118                Box::new(Condition::Flag("b")),
2119            )
2120        );
2121    }
2122
2123    #[test]
2124    fn parse_condition_empty() {
2125        let c = parse_condition("");
2126        assert_eq!(c, Condition::Raw(""));
2127    }
2128
2129    // -- Full AST derivation tests --------------------------------------------
2130
2131    #[test]
2132    fn derive_minimal_file() {
2133        let src = "cabal-version: 3.0\nname: my-pkg\nversion: 0.1.0.0\n";
2134        let result = do_parse(src);
2135        let ast = derive_ast(&result.cst);
2136
2137        assert_eq!(ast.name, Some("my-pkg"));
2138        assert_eq!(
2139            ast.version,
2140            Some(Version {
2141                components: vec![0, 1, 0, 0]
2142            })
2143        );
2144        assert!(ast.cabal_version.is_some());
2145        let cv = ast.cabal_version.as_ref().unwrap();
2146        assert_eq!(cv.raw, "3.0");
2147        assert_eq!(
2148            cv.version,
2149            Some(Version {
2150                components: vec![3, 0]
2151            })
2152        );
2153    }
2154
2155    #[test]
2156    fn derive_with_library() {
2157        let src = "\
2158cabal-version: 3.0
2159name: my-pkg
2160version: 0.1.0.0
2161
2162library
2163  exposed-modules:
2164    Foo
2165    Bar
2166  build-depends:
2167    base >=4.14
2168  default-language: GHC2021
2169";
2170        let result = do_parse(src);
2171        let ast = derive_ast(&result.cst);
2172
2173        assert!(ast.library.is_some());
2174        let lib = ast.library.as_ref().unwrap();
2175        assert_eq!(lib.exposed_modules, vec!["Foo", "Bar"]);
2176        assert_eq!(lib.fields.build_depends.len(), 1);
2177        assert_eq!(lib.fields.build_depends[0].package, "base");
2178        assert_eq!(lib.fields.default_language, Some("GHC2021"));
2179    }
2180
2181    #[test]
2182    fn derive_with_executable() {
2183        let src = "\
2184cabal-version: 3.0
2185name: my-pkg
2186version: 0.1.0.0
2187
2188executable my-exe
2189  main-is: Main.hs
2190  build-depends: base
2191  hs-source-dirs: app
2192";
2193        let result = do_parse(src);
2194        let ast = derive_ast(&result.cst);
2195
2196        assert_eq!(ast.executables.len(), 1);
2197        let exe = &ast.executables[0];
2198        assert_eq!(exe.fields.name, Some("my-exe"));
2199        assert_eq!(exe.main_is, Some("Main.hs"));
2200        assert_eq!(exe.fields.build_depends.len(), 1);
2201        assert_eq!(exe.fields.hs_source_dirs, vec!["app"]);
2202    }
2203
2204    #[test]
2205    fn derive_with_test_suite() {
2206        let src = "\
2207cabal-version: 3.0
2208name: my-pkg
2209version: 0.1.0.0
2210
2211test-suite my-tests
2212  type: exitcode-stdio-1.0
2213  main-is: Main.hs
2214  build-depends: base, tasty
2215";
2216        let result = do_parse(src);
2217        let ast = derive_ast(&result.cst);
2218
2219        assert_eq!(ast.test_suites.len(), 1);
2220        let ts = &ast.test_suites[0];
2221        assert_eq!(ts.fields.name, Some("my-tests"));
2222        assert_eq!(ts.test_type, Some("exitcode-stdio-1.0"));
2223        assert_eq!(ts.main_is, Some("Main.hs"));
2224        assert_eq!(ts.fields.build_depends.len(), 2);
2225    }
2226
2227    #[test]
2228    fn derive_with_common_stanza() {
2229        let src = "\
2230cabal-version: 3.0
2231name: my-pkg
2232version: 0.1.0.0
2233
2234common warnings
2235  ghc-options: -Wall -Wcompat
2236
2237library
2238  import: warnings
2239  exposed-modules: Foo
2240";
2241        let result = do_parse(src);
2242        let ast = derive_ast(&result.cst);
2243
2244        assert_eq!(ast.common_stanzas.len(), 1);
2245        assert_eq!(ast.common_stanzas[0].name, "warnings");
2246        assert_eq!(
2247            ast.common_stanzas[0].fields.ghc_options,
2248            vec!["-Wall", "-Wcompat"]
2249        );
2250
2251        let lib = ast.library.as_ref().unwrap();
2252        assert_eq!(lib.fields.imports, vec!["warnings"]);
2253    }
2254
2255    #[test]
2256    fn derive_with_flag() {
2257        let src = "\
2258cabal-version: 3.0
2259name: my-pkg
2260version: 0.1.0.0
2261
2262flag dev
2263  description: Development mode
2264  default: False
2265  manual: True
2266";
2267        let result = do_parse(src);
2268        let ast = derive_ast(&result.cst);
2269
2270        assert_eq!(ast.flags.len(), 1);
2271        let flag = &ast.flags[0];
2272        assert_eq!(flag.name, "dev");
2273        assert_eq!(flag.description, Some("Development mode"));
2274        assert_eq!(flag.default, Some(false));
2275        assert_eq!(flag.manual, Some(true));
2276    }
2277
2278    #[test]
2279    fn derive_with_source_repository() {
2280        let src = "\
2281cabal-version: 3.0
2282name: my-pkg
2283version: 0.1.0.0
2284
2285source-repository head
2286  type: git
2287  location: https://github.com/example/my-pkg
2288";
2289        let result = do_parse(src);
2290        let ast = derive_ast(&result.cst);
2291
2292        assert_eq!(ast.source_repositories.len(), 1);
2293        let sr = &ast.source_repositories[0];
2294        assert_eq!(sr.kind, Some("head"));
2295        assert_eq!(sr.repo_type, Some("git"));
2296        assert_eq!(sr.location, Some("https://github.com/example/my-pkg"));
2297    }
2298
2299    #[test]
2300    fn derive_conditional() {
2301        let src = "\
2302cabal-version: 3.0
2303name: my-pkg
2304version: 0.1.0.0
2305
2306library
2307  build-depends: base
2308  if flag(dev)
2309    ghc-options: -O0
2310  else
2311    ghc-options: -O2
2312";
2313        let result = do_parse(src);
2314        let ast = derive_ast(&result.cst);
2315
2316        let lib = ast.library.as_ref().unwrap();
2317        assert_eq!(lib.fields.conditionals.len(), 1);
2318        let cond = &lib.fields.conditionals[0];
2319        assert_eq!(cond.condition, Condition::Flag("dev"));
2320        assert_eq!(cond.then_fields.len(), 1);
2321        assert_eq!(cond.then_fields[0].name, "ghc-options");
2322        assert_eq!(cond.then_fields[0].value, "-O0");
2323        assert_eq!(cond.else_fields.len(), 1);
2324        assert_eq!(cond.else_fields[0].name, "ghc-options");
2325        assert_eq!(cond.else_fields[0].value, "-O2");
2326    }
2327
2328    #[test]
2329    fn derive_all_dependencies() {
2330        let src = "\
2331cabal-version: 3.0
2332name: my-pkg
2333version: 0.1.0.0
2334
2335library
2336  build-depends: base, text
2337
2338executable my-exe
2339  build-depends: base, my-pkg
2340";
2341        let result = do_parse(src);
2342        let ast = derive_ast(&result.cst);
2343
2344        let all_deps = ast.all_dependencies();
2345        assert_eq!(all_deps.len(), 4);
2346        let names: Vec<&str> = all_deps.iter().map(|d| d.package).collect();
2347        assert!(names.contains(&"base"));
2348        assert!(names.contains(&"text"));
2349        assert!(names.contains(&"my-pkg"));
2350    }
2351
2352    #[test]
2353    fn derive_all_components() {
2354        let src = "\
2355cabal-version: 3.0
2356name: my-pkg
2357version: 0.1.0.0
2358
2359library
2360  exposed-modules: Foo
2361
2362executable my-exe
2363  main-is: Main.hs
2364
2365test-suite my-tests
2366  type: exitcode-stdio-1.0
2367  main-is: Main.hs
2368
2369benchmark my-bench
2370  type: exitcode-stdio-1.0
2371  main-is: Main.hs
2372";
2373        let result = do_parse(src);
2374        let ast = derive_ast(&result.cst);
2375
2376        let comps = ast.all_components();
2377        assert_eq!(comps.len(), 4);
2378    }
2379
2380    #[test]
2381    fn derive_find_component() {
2382        let src = "\
2383cabal-version: 3.0
2384name: my-pkg
2385version: 0.1.0.0
2386
2387library
2388  exposed-modules: Foo
2389
2390executable my-exe
2391  main-is: Main.hs
2392";
2393        let result = do_parse(src);
2394        let ast = derive_ast(&result.cst);
2395
2396        assert!(ast.find_component("library").is_some());
2397        assert!(ast.find_component("my-exe").is_some());
2398        assert!(ast.find_component("nonexistent").is_none());
2399    }
2400
2401    #[test]
2402    fn derive_cst_node_back_references_valid() {
2403        let src = "\
2404cabal-version: 3.0
2405name: my-pkg
2406version: 0.1.0.0
2407
2408library
2409  build-depends: base >=4.14
2410";
2411        let result = do_parse(src);
2412        let ast = derive_ast(&result.cst);
2413
2414        // The CST root back-reference should be valid.
2415        assert_eq!(ast.cst_root, result.cst.root);
2416
2417        // Library's cst_node should be a valid Section node.
2418        let lib = ast.library.as_ref().unwrap();
2419        let node = result.cst.node(lib.fields.cst_node);
2420        assert_eq!(node.kind, CstNodeKind::Section);
2421
2422        // Dependency's cst_node should be valid.
2423        assert!(!lib.fields.build_depends.is_empty());
2424        let dep_node_id = lib.fields.build_depends[0].cst_node;
2425        assert!(dep_node_id.0 < result.cst.node_count());
2426    }
2427
2428    #[test]
2429    fn derive_deps_leading_comma_style() {
2430        let src = "\
2431cabal-version: 3.0
2432name: my-pkg
2433version: 0.1.0.0
2434
2435library
2436  build-depends:
2437      base >=4.14
2438    , text >=2.0
2439    , aeson ^>=2.2
2440";
2441        let result = do_parse(src);
2442        let ast = derive_ast(&result.cst);
2443
2444        let lib = ast.library.as_ref().unwrap();
2445        assert_eq!(lib.fields.build_depends.len(), 3);
2446        assert_eq!(lib.fields.build_depends[0].package, "base");
2447        assert_eq!(lib.fields.build_depends[1].package, "text");
2448        assert_eq!(lib.fields.build_depends[2].package, "aeson");
2449    }
2450
2451    #[test]
2452    fn derive_deps_trailing_comma_style() {
2453        let src = "\
2454cabal-version: 3.0
2455name: my-pkg
2456version: 0.1.0.0
2457
2458library
2459  build-depends:
2460    base >=4.14,
2461    text >=2.0,
2462    aeson ^>=2.2
2463";
2464        let result = do_parse(src);
2465        let ast = derive_ast(&result.cst);
2466
2467        let lib = ast.library.as_ref().unwrap();
2468        assert_eq!(lib.fields.build_depends.len(), 3);
2469        assert_eq!(lib.fields.build_depends[0].package, "base");
2470        assert_eq!(lib.fields.build_depends[1].package, "text");
2471        assert_eq!(lib.fields.build_depends[2].package, "aeson");
2472    }
2473
2474    #[test]
2475    fn derive_deps_single_line() {
2476        let src = "\
2477cabal-version: 3.0
2478name: my-pkg
2479version: 0.1.0.0
2480
2481library
2482  build-depends: base >=4.14, text >=2.0, aeson ^>=2.2
2483";
2484        let result = do_parse(src);
2485        let ast = derive_ast(&result.cst);
2486
2487        let lib = ast.library.as_ref().unwrap();
2488        assert_eq!(lib.fields.build_depends.len(), 3);
2489    }
2490
2491    #[test]
2492    fn derive_default_extensions() {
2493        let src = "\
2494cabal-version: 3.0
2495name: my-pkg
2496version: 0.1.0.0
2497
2498library
2499  default-extensions:
2500    OverloadedStrings
2501    DerivingStrategies
2502";
2503        let result = do_parse(src);
2504        let ast = derive_ast(&result.cst);
2505
2506        let lib = ast.library.as_ref().unwrap();
2507        assert_eq!(
2508            lib.fields.default_extensions,
2509            vec!["OverloadedStrings", "DerivingStrategies"]
2510        );
2511    }
2512
2513    #[test]
2514    fn derive_metadata_fields() {
2515        let src = "\
2516cabal-version: 3.0
2517name: my-pkg
2518version: 0.1.0.0
2519license: MIT
2520synopsis: A test package
2521author: Test Author
2522maintainer: test@example.com
2523homepage: https://example.com
2524bug-reports: https://example.com/issues
2525category: Development
2526build-type: Simple
2527";
2528        let result = do_parse(src);
2529        let ast = derive_ast(&result.cst);
2530
2531        assert_eq!(ast.license, Some("MIT"));
2532        assert_eq!(ast.synopsis, Some("A test package"));
2533        assert_eq!(ast.author, Some("Test Author"));
2534        assert_eq!(ast.maintainer, Some("test@example.com"));
2535        assert_eq!(ast.homepage, Some("https://example.com"));
2536        assert_eq!(ast.bug_reports, Some("https://example.com/issues"));
2537        assert_eq!(ast.category, Some("Development"));
2538        assert_eq!(ast.build_type, Some("Simple"));
2539    }
2540
2541    #[test]
2542    fn derive_conditional_deps() {
2543        let src = "\
2544cabal-version: 3.0
2545name: my-pkg
2546version: 0.1.0.0
2547
2548library
2549  build-depends: base
2550  if os(windows)
2551    build-depends: Win32
2552  else
2553    build-depends: unix
2554";
2555        let result = do_parse(src);
2556        let ast = derive_ast(&result.cst);
2557
2558        let all_deps = ast.all_dependencies();
2559        let names: Vec<&str> = all_deps.iter().map(|d| d.package).collect();
2560        assert!(names.contains(&"base"));
2561        assert!(names.contains(&"Win32"));
2562        assert!(names.contains(&"unix"));
2563        assert_eq!(all_deps.len(), 3);
2564    }
2565
2566    // -- Boolean literal condition tests ----------------------------------------
2567
2568    #[test]
2569    fn parse_condition_true() {
2570        assert_eq!(parse_condition("true"), Condition::Lit(true));
2571    }
2572
2573    #[test]
2574    fn parse_condition_false() {
2575        assert_eq!(parse_condition("false"), Condition::Lit(false));
2576    }
2577
2578    #[test]
2579    fn parse_condition_true_case_insensitive() {
2580        assert_eq!(parse_condition("True"), Condition::Lit(true));
2581        assert_eq!(parse_condition("FALSE"), Condition::Lit(false));
2582    }
2583
2584    // -- Wildcard version range tests -------------------------------------------
2585
2586    #[test]
2587    fn version_range_wildcard() {
2588        let r = parse_version_range("==1.2.*").unwrap();
2589        match r {
2590            VersionRange::And(a, b) => {
2591                assert_eq!(
2592                    *a,
2593                    VersionRange::Gte(Version {
2594                        components: vec![1, 2]
2595                    })
2596                );
2597                assert_eq!(
2598                    *b,
2599                    VersionRange::Lt(Version {
2600                        components: vec![1, 3]
2601                    })
2602                );
2603            }
2604            _ => panic!("Expected And range, got {:?}", r),
2605        }
2606    }
2607
2608    // -- -any and -none version range tests -------------------------------------
2609
2610    #[test]
2611    fn version_range_any_keyword() {
2612        assert_eq!(parse_version_range("-any").unwrap(), VersionRange::Any);
2613    }
2614
2615    #[test]
2616    fn version_range_none_keyword() {
2617        assert_eq!(
2618            parse_version_range("-none").unwrap(),
2619            VersionRange::NoVersion
2620        );
2621    }
2622
2623    // -- Set notation version range tests ---------------------------------------
2624
2625    #[test]
2626    fn version_range_set_major_bound() {
2627        let r = parse_version_range("^>= { 2.6, 2.7, 2.8 }").unwrap();
2628        match r {
2629            VersionRange::Or(_, _) => {} // just verify it parses as Or
2630            _ => panic!("Expected Or range for set notation, got {:?}", r),
2631        }
2632    }
2633
2634    #[test]
2635    fn version_range_set_eq() {
2636        let r = parse_version_range("== { 1.0, 2.0 }").unwrap();
2637        match r {
2638            VersionRange::Or(_, _) => {}
2639            _ => panic!("Expected Or range for set notation, got {:?}", r),
2640        }
2641    }
2642
2643    // -- Display tests for new variants -----------------------------------------
2644
2645    #[test]
2646    fn version_range_display_any() {
2647        assert_eq!(VersionRange::Any.to_string(), "-any");
2648    }
2649
2650    #[test]
2651    fn version_range_display_none() {
2652        assert_eq!(VersionRange::NoVersion.to_string(), "-none");
2653    }
2654
2655    #[test]
2656    fn derive_benchmark() {
2657        let src = "\
2658cabal-version: 3.0
2659name: my-pkg
2660version: 0.1.0.0
2661
2662benchmark my-bench
2663  type: exitcode-stdio-1.0
2664  main-is: Main.hs
2665  build-depends: base, criterion
2666  hs-source-dirs: bench
2667";
2668        let result = do_parse(src);
2669        let ast = derive_ast(&result.cst);
2670
2671        assert_eq!(ast.benchmarks.len(), 1);
2672        let bm = &ast.benchmarks[0];
2673        assert_eq!(bm.fields.name, Some("my-bench"));
2674        assert_eq!(bm.bench_type, Some("exitcode-stdio-1.0"));
2675        assert_eq!(bm.main_is, Some("Main.hs"));
2676        assert_eq!(bm.fields.build_depends.len(), 2);
2677    }
2678}