use crate::cst::{CabalCst, CstNodeKind};
use crate::span::NodeId;
pub fn canonicalize_field_name(name: &str) -> String {
name.to_ascii_lowercase().replace('_', "-")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Version {
pub components: Vec<u64>,
}
impl Version {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
if s.is_empty() {
return None;
}
let mut components = Vec::new();
for part in s.split('.') {
let part = part.trim();
if part.is_empty() {
return None;
}
match part.parse::<u64>() {
Ok(n) => components.push(n),
Err(_) => return None,
}
}
if components.is_empty() {
return None;
}
Some(Version { components })
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for c in &self.components {
if !first {
write!(f, ".")?;
}
write!(f, "{c}")?;
first = false;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionRange {
Any,
NoVersion,
Eq(Version),
Gt(Version),
Gte(Version),
Lt(Version),
Lte(Version),
MajorBound(Version),
And(Box<VersionRange>, Box<VersionRange>),
Or(Box<VersionRange>, Box<VersionRange>),
}
impl std::fmt::Display for VersionRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionRange::Any => write!(f, "-any"),
VersionRange::NoVersion => write!(f, "-none"),
VersionRange::Eq(v) => write!(f, "=={v}"),
VersionRange::Gt(v) => write!(f, ">{v}"),
VersionRange::Gte(v) => write!(f, ">={v}"),
VersionRange::Lt(v) => write!(f, "<{v}"),
VersionRange::Lte(v) => write!(f, "<={v}"),
VersionRange::MajorBound(v) => write!(f, "^>={v}"),
VersionRange::And(a, b) => write!(f, "{a} && {b}"),
VersionRange::Or(a, b) => write!(f, "{a} || {b}"),
}
}
}
impl VersionRange {
pub fn satisfies(&self, version: &Version) -> bool {
version_satisfies(version, self)
}
}
pub fn version_satisfies(version: &Version, vr: &VersionRange) -> bool {
use std::cmp::Ordering;
let cmp_versions = |a: &Version, b: &Version| -> Ordering {
let max_len = a.components.len().max(b.components.len());
for i in 0..max_len {
let ac = a.components.get(i).copied().unwrap_or(0);
let bc = b.components.get(i).copied().unwrap_or(0);
match ac.cmp(&bc) {
Ordering::Equal => continue,
other => return other,
}
}
Ordering::Equal
};
match vr {
VersionRange::Any => true,
VersionRange::NoVersion => false,
VersionRange::Eq(v) => cmp_versions(version, v) == Ordering::Equal,
VersionRange::Gt(v) => cmp_versions(version, v) == Ordering::Greater,
VersionRange::Gte(v) => cmp_versions(version, v) != Ordering::Less,
VersionRange::Lt(v) => cmp_versions(version, v) == Ordering::Less,
VersionRange::Lte(v) => cmp_versions(version, v) != Ordering::Greater,
VersionRange::MajorBound(v) => {
if cmp_versions(version, v) == Ordering::Less {
return false;
}
let mut upper = v.clone();
if upper.components.len() >= 2 {
upper.components[1] += 1;
upper.components.truncate(2);
} else if upper.components.len() == 1 {
upper.components[0] += 1;
}
cmp_versions(version, &upper) == Ordering::Less
}
VersionRange::And(a, b) => version_satisfies(version, a) && version_satisfies(version, b),
VersionRange::Or(a, b) => version_satisfies(version, a) || version_satisfies(version, b),
}
}
pub fn parse_version_range(s: &str) -> Option<VersionRange> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some(range) = parse_or_range(s) {
return Some(range);
}
None
}
fn parse_or_range(s: &str) -> Option<VersionRange> {
let parts = split_respecting_parens(s, "||");
if parts.len() > 1 {
let mut ranges: Vec<VersionRange> = Vec::new();
for part in &parts {
ranges.push(parse_and_range(part.trim())?);
}
let mut result = ranges.remove(0);
for r in ranges {
result = VersionRange::Or(Box::new(result), Box::new(r));
}
return Some(result);
}
parse_and_range(s)
}
fn parse_and_range(s: &str) -> Option<VersionRange> {
let parts = split_respecting_parens(s, "&&");
if parts.len() > 1 {
let mut ranges: Vec<VersionRange> = Vec::new();
for part in &parts {
ranges.push(parse_atom_range(part.trim())?);
}
let mut result = ranges.remove(0);
for r in ranges {
result = VersionRange::And(Box::new(result), Box::new(r));
}
return Some(result);
}
parse_atom_range(s)
}
fn parse_atom_range(s: &str) -> Option<VersionRange> {
let s = s.trim();
if s.is_empty() {
return None;
}
if s.eq_ignore_ascii_case("-any") {
return Some(VersionRange::Any);
}
if s.eq_ignore_ascii_case("-none") {
return Some(VersionRange::NoVersion);
}
if s.starts_with('(') && s.ends_with(')') {
return parse_or_range(&s[1..s.len() - 1]);
}
if let Some(rest) = s.strip_prefix("^>=") {
let rest = rest.trim();
if rest.starts_with('{') && rest.ends_with('}') {
let inner = &rest[1..rest.len() - 1];
let versions: Vec<&str> = inner.split(',').map(|v| v.trim()).collect();
let mut ranges: Vec<VersionRange> = Vec::new();
for v_str in versions {
if !v_str.is_empty() {
if let Some(v) = Version::parse(v_str) {
ranges.push(VersionRange::MajorBound(v));
}
}
}
if ranges.is_empty() {
return None;
}
let mut result = ranges.remove(0);
for r in ranges {
result = VersionRange::Or(Box::new(result), Box::new(r));
}
return Some(result);
}
let v = Version::parse(rest)?;
return Some(VersionRange::MajorBound(v));
}
if let Some(rest) = s.strip_prefix(">=") {
let v = Version::parse(rest.trim())?;
return Some(VersionRange::Gte(v));
}
if let Some(rest) = s.strip_prefix("<=") {
let v = Version::parse(rest.trim())?;
return Some(VersionRange::Lte(v));
}
if let Some(rest) = s.strip_prefix("==") {
let rest = rest.trim();
if rest.starts_with('{') && rest.ends_with('}') {
let inner = &rest[1..rest.len() - 1];
let versions: Vec<&str> = inner.split(',').map(|v| v.trim()).collect();
let mut ranges: Vec<VersionRange> = Vec::new();
for v_str in versions {
if !v_str.is_empty() {
if let Some(v) = Version::parse(v_str) {
ranges.push(VersionRange::Eq(v));
}
}
}
if ranges.is_empty() {
return None;
}
let mut result = ranges.remove(0);
for r in ranges {
result = VersionRange::Or(Box::new(result), Box::new(r));
}
return Some(result);
}
if let Some(prefix) = rest.strip_suffix(".*") {
let v = Version::parse(prefix)?;
let mut upper = v.clone();
if let Some(last) = upper.components.last_mut() {
*last += 1;
}
return Some(VersionRange::And(
Box::new(VersionRange::Gte(v)),
Box::new(VersionRange::Lt(upper)),
));
}
let v = Version::parse(rest)?;
return Some(VersionRange::Eq(v));
}
if let Some(rest) = s.strip_prefix('>') {
let v = Version::parse(rest.trim())?;
return Some(VersionRange::Gt(v));
}
if let Some(rest) = s.strip_prefix('<') {
let v = Version::parse(rest.trim())?;
return Some(VersionRange::Lt(v));
}
None
}
fn split_respecting_parens<'a>(s: &'a str, delim: &str) -> Vec<&'a str> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut last = 0;
let bytes = s.as_bytes();
let delim_bytes = delim.as_bytes();
let delim_len = delim_bytes.len();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'(' {
depth += 1;
i += 1;
} else if bytes[i] == b')' {
depth = depth.saturating_sub(1);
i += 1;
} else if depth == 0
&& i + delim_len <= bytes.len()
&& &bytes[i..i + delim_len] == delim_bytes
{
parts.push(&s[last..i]);
i += delim_len;
last = i;
} else {
i += 1;
}
}
parts.push(&s[last..]);
parts
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dependency<'a> {
pub package: &'a str,
pub version_range: Option<VersionRange>,
pub cst_node: NodeId,
}
fn parse_single_dependency<'a>(s: &'a str, cst_node: NodeId) -> Option<Dependency<'a>> {
let s = s.trim();
if s.is_empty() {
return None;
}
let name_end = s
.find(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
.unwrap_or(s.len());
let package = s[..name_end].trim();
if package.is_empty() {
return None;
}
let rest = s[name_end..].trim();
let version_range = if rest.is_empty() {
None
} else {
parse_version_range(rest)
};
Some(Dependency {
package,
version_range,
cst_node,
})
}
fn parse_dependencies_from_text<'a>(text: &'a str, cst_node: NodeId) -> Vec<Dependency<'a>> {
let mut deps = Vec::new();
for part in text.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(dep) = parse_single_dependency(part, cst_node) {
deps.push(dep);
}
}
deps
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CabalVersion<'a> {
pub raw: &'a str,
pub version: Option<Version>,
pub cst_node: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field<'a> {
pub name: String,
pub raw_name: &'a str,
pub value: String,
pub cst_node: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Condition<'a> {
Flag(&'a str),
OS(&'a str),
Arch(&'a str),
Impl(&'a str, Option<VersionRange>),
Not(Box<Condition<'a>>),
And(Box<Condition<'a>>, Box<Condition<'a>>),
Or(Box<Condition<'a>>, Box<Condition<'a>>),
Lit(bool),
Raw(&'a str),
}
pub fn parse_condition(s: &str) -> Condition<'_> {
let s = s.trim();
if s.is_empty() {
return Condition::Raw(s);
}
match parse_condition_or(s) {
Some(c) => c,
None => Condition::Raw(s),
}
}
fn parse_condition_or(s: &str) -> Option<Condition<'_>> {
let parts = split_respecting_parens(s, "||");
if parts.len() > 1 {
let mut conds: Vec<Condition<'_>> = Vec::new();
for part in &parts {
conds.push(parse_condition_and(part.trim())?);
}
let mut result = conds.remove(0);
for c in conds {
result = Condition::Or(Box::new(result), Box::new(c));
}
return Some(result);
}
parse_condition_and(s)
}
fn parse_condition_and(s: &str) -> Option<Condition<'_>> {
let parts = split_respecting_parens(s, "&&");
if parts.len() > 1 {
let mut conds: Vec<Condition<'_>> = Vec::new();
for part in &parts {
conds.push(parse_condition_atom(part.trim())?);
}
let mut result = conds.remove(0);
for c in conds {
result = Condition::And(Box::new(result), Box::new(c));
}
return Some(result);
}
parse_condition_atom(s)
}
fn parse_condition_atom(s: &str) -> Option<Condition<'_>> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some(rest) = s.strip_prefix('!') {
let inner = parse_condition_atom(rest.trim())?;
return Some(Condition::Not(Box::new(inner)));
}
if s.starts_with('(') && s.ends_with(')') {
return parse_condition_or(&s[1..s.len() - 1]);
}
if let Some(paren_start) = s.find('(') {
if s.ends_with(')') {
let func = s[..paren_start].trim();
let arg = s[paren_start + 1..s.len() - 1].trim();
let func_lower = func.to_ascii_lowercase();
match func_lower.as_str() {
"flag" => return Some(Condition::Flag(arg)),
"os" => return Some(Condition::OS(arg)),
"arch" => return Some(Condition::Arch(arg)),
"impl" => {
let parts: Vec<&str> = arg.splitn(2, char::is_whitespace).collect();
let compiler = parts[0];
let vr = if parts.len() > 1 {
parse_version_range(parts[1].trim())
} else {
None
};
return Some(Condition::Impl(compiler, vr));
}
_ => {}
}
}
}
match s.to_ascii_lowercase().as_str() {
"true" => return Some(Condition::Lit(true)),
"false" => return Some(Condition::Lit(false)),
_ => {}
}
Some(Condition::Raw(s))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conditional<'a> {
pub condition: Condition<'a>,
pub then_fields: Vec<Field<'a>>,
pub then_deps: Vec<Dependency<'a>>,
pub else_fields: Vec<Field<'a>>,
pub else_deps: Vec<Dependency<'a>>,
pub then_conditionals: Vec<Conditional<'a>>,
pub else_conditionals: Vec<Conditional<'a>>,
pub cst_node: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentFields<'a> {
pub name: Option<&'a str>,
pub cst_node: NodeId,
pub imports: Vec<&'a str>,
pub build_depends: Vec<Dependency<'a>>,
pub other_modules: Vec<&'a str>,
pub hs_source_dirs: Vec<&'a str>,
pub default_language: Option<&'a str>,
pub default_extensions: Vec<&'a str>,
pub ghc_options: Vec<&'a str>,
pub other_fields: Vec<Field<'a>>,
pub conditionals: Vec<Conditional<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Library<'a> {
pub fields: ComponentFields<'a>,
pub exposed_modules: Vec<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Executable<'a> {
pub fields: ComponentFields<'a>,
pub main_is: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestSuite<'a> {
pub fields: ComponentFields<'a>,
pub test_type: Option<&'a str>,
pub main_is: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Benchmark<'a> {
pub fields: ComponentFields<'a>,
pub bench_type: Option<&'a str>,
pub main_is: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommonStanza<'a> {
pub name: &'a str,
pub fields: ComponentFields<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Flag<'a> {
pub name: &'a str,
pub description: Option<&'a str>,
pub default: Option<bool>,
pub manual: Option<bool>,
pub other_fields: Vec<Field<'a>>,
pub cst_node: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceRepository<'a> {
pub kind: Option<&'a str>,
pub repo_type: Option<&'a str>,
pub location: Option<&'a str>,
pub tag: Option<&'a str>,
pub branch: Option<&'a str>,
pub subdir: Option<&'a str>,
pub other_fields: Vec<Field<'a>>,
pub cst_node: NodeId,
}
#[derive(Debug, Clone)]
pub enum Component<'a, 'b> {
Library(&'b Library<'a>),
Executable(&'b Executable<'a>),
TestSuite(&'b TestSuite<'a>),
Benchmark(&'b Benchmark<'a>),
}
impl<'a, 'b> Component<'a, 'b> {
pub fn fields(&self) -> &ComponentFields<'a> {
match self {
Component::Library(l) => &l.fields,
Component::Executable(e) => &e.fields,
Component::TestSuite(t) => &t.fields,
Component::Benchmark(b) => &b.fields,
}
}
pub fn name(&self) -> Option<&'a str> {
self.fields().name
}
pub fn cst_node(&self) -> NodeId {
self.fields().cst_node
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CabalFile<'a> {
pub source: &'a str,
pub cabal_version: Option<CabalVersion<'a>>,
pub name: Option<&'a str>,
pub version: Option<Version>,
pub license: Option<&'a str>,
pub synopsis: Option<&'a str>,
pub description: Option<&'a str>,
pub author: Option<&'a str>,
pub maintainer: Option<&'a str>,
pub homepage: Option<&'a str>,
pub bug_reports: Option<&'a str>,
pub category: Option<&'a str>,
pub build_type: Option<&'a str>,
pub tested_with: Option<&'a str>,
pub extra_source_files: Vec<&'a str>,
pub other_fields: Vec<Field<'a>>,
pub common_stanzas: Vec<CommonStanza<'a>>,
pub flags: Vec<Flag<'a>>,
pub library: Option<Library<'a>>,
pub named_libraries: Vec<Library<'a>>,
pub executables: Vec<Executable<'a>>,
pub test_suites: Vec<TestSuite<'a>>,
pub benchmarks: Vec<Benchmark<'a>>,
pub source_repositories: Vec<SourceRepository<'a>>,
pub cst_root: NodeId,
}
impl<'a> CabalFile<'a> {
pub fn all_dependencies(&self) -> Vec<&Dependency<'a>> {
let mut deps = Vec::new();
if let Some(ref lib) = self.library {
collect_component_deps(&lib.fields, &mut deps);
}
for lib in &self.named_libraries {
collect_component_deps(&lib.fields, &mut deps);
}
for exe in &self.executables {
collect_component_deps(&exe.fields, &mut deps);
}
for ts in &self.test_suites {
collect_component_deps(&ts.fields, &mut deps);
}
for bm in &self.benchmarks {
collect_component_deps(&bm.fields, &mut deps);
}
for cs in &self.common_stanzas {
collect_component_deps(&cs.fields, &mut deps);
}
deps
}
pub fn all_components(&self) -> Vec<Component<'a, '_>> {
let mut comps = Vec::new();
if let Some(ref lib) = self.library {
comps.push(Component::Library(lib));
}
for lib in &self.named_libraries {
comps.push(Component::Library(lib));
}
for exe in &self.executables {
comps.push(Component::Executable(exe));
}
for ts in &self.test_suites {
comps.push(Component::TestSuite(ts));
}
for bm in &self.benchmarks {
comps.push(Component::Benchmark(bm));
}
comps
}
pub fn find_component(&self, name: &str) -> Option<Component<'a, '_>> {
if let Some(ref lib) = self.library {
if name == "library" || lib.fields.name == Some(name) {
return Some(Component::Library(lib));
}
}
for lib in &self.named_libraries {
if lib.fields.name == Some(name) {
return Some(Component::Library(lib));
}
}
for exe in &self.executables {
if exe.fields.name == Some(name) {
return Some(Component::Executable(exe));
}
}
for ts in &self.test_suites {
if ts.fields.name == Some(name) {
return Some(Component::TestSuite(ts));
}
}
for bm in &self.benchmarks {
if bm.fields.name == Some(name) {
return Some(Component::Benchmark(bm));
}
}
None
}
}
fn collect_component_deps<'a, 'b>(
fields: &'b ComponentFields<'a>,
deps: &mut Vec<&'b Dependency<'a>>,
) {
for d in &fields.build_depends {
deps.push(d);
}
collect_conditional_deps(&fields.conditionals, deps);
}
fn collect_conditional_deps<'a, 'b>(
conditionals: &'b [Conditional<'a>],
deps: &mut Vec<&'b Dependency<'a>>,
) {
for cond in conditionals {
for d in &cond.then_deps {
deps.push(d);
}
for d in &cond.else_deps {
deps.push(d);
}
collect_conditional_deps(&cond.then_conditionals, deps);
collect_conditional_deps(&cond.else_conditionals, deps);
}
}
pub fn derive_ast<'a>(cst: &'a CabalCst) -> CabalFile<'a> {
let source = cst.source.as_str();
let mut file = CabalFile {
source,
cabal_version: None,
name: None,
version: None,
license: None,
synopsis: None,
description: None,
author: None,
maintainer: None,
homepage: None,
bug_reports: None,
category: None,
build_type: None,
tested_with: None,
extra_source_files: Vec::new(),
other_fields: Vec::new(),
common_stanzas: Vec::new(),
flags: Vec::new(),
library: None,
named_libraries: Vec::new(),
executables: Vec::new(),
test_suites: Vec::new(),
benchmarks: Vec::new(),
source_repositories: Vec::new(),
cst_root: cst.root,
};
collect_ast_nodes(cst, cst.root, &mut file);
file
}
fn collect_ast_nodes<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
let node = cst.node(node_id);
match node.kind {
CstNodeKind::Root => {
let children: Vec<NodeId> = node.children.clone();
for child_id in children {
collect_ast_nodes(cst, child_id, file);
}
}
CstNodeKind::Field if cst.node(node_id).parent == Some(cst.root) => {
derive_top_level_field(cst, node_id, file);
}
CstNodeKind::Field => {}
CstNodeKind::Section => {
let source = cst.source.as_str();
let is_top_level_section = if let Some(ref kw_span) = node.section_keyword {
let kw = kw_span.slice(source).to_ascii_lowercase();
matches!(
kw.as_str(),
"library"
| "executable"
| "test-suite"
| "benchmark"
| "common"
| "flag"
| "source-repository"
)
} else {
false
};
if is_top_level_section {
derive_section(cst, node_id, file);
let children: Vec<NodeId> = cst.node(node_id).children.clone();
for child_id in children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::Section {
collect_ast_nodes(cst, child_id, file);
}
}
}
}
_ => {}
}
}
fn field_full_value(cst: &CabalCst, node_id: NodeId) -> String {
let node = cst.node(node_id);
let source = cst.source.as_str();
let mut value = String::new();
if let Some(ref val_span) = node.field_value {
value.push_str(val_span.slice(source).trim());
}
for &child_id in &node.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::ValueLine {
let line_text = child.content_span.slice(source).trim();
if !line_text.is_empty() {
if !value.is_empty() {
value.push('\n');
}
value.push_str(line_text);
}
}
}
value
}
fn field_first_line_value(cst: &CabalCst, node_id: NodeId) -> Option<&str> {
let node = cst.node(node_id);
let source = cst.source.as_str();
if let Some(ref val_span) = node.field_value {
let v = val_span.slice(source).trim();
if !v.is_empty() {
return Some(v);
}
}
for &child_id in &node.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::ValueLine {
let v = child.content_span.slice(source).trim();
if !v.is_empty() {
return Some(v);
}
}
}
None
}
fn parse_list_field(cst: &CabalCst, node_id: NodeId) -> Vec<&str> {
let node = cst.node(node_id);
let source = cst.source.as_str();
let mut items = Vec::new();
if let Some(ref val_span) = node.field_value {
let text = val_span.slice(source).trim();
for item in split_list_items(text) {
if !item.is_empty() {
items.push(item);
}
}
}
for &child_id in &node.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::ValueLine {
let text = child.content_span.slice(source).trim();
for item in split_list_items(text) {
if !item.is_empty() {
items.push(item);
}
}
}
}
items
}
fn split_list_items(text: &str) -> Vec<&str> {
let mut items = Vec::new();
if text.contains(',') {
for part in text.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
items.push(trimmed);
}
}
} else {
for part in text.split_whitespace() {
items.push(part);
}
}
items
}
fn parse_ghc_options(cst: &CabalCst, node_id: NodeId) -> Vec<&str> {
let node = cst.node(node_id);
let source = cst.source.as_str();
let mut opts = Vec::new();
if let Some(ref val_span) = node.field_value {
for opt in val_span.slice(source).split_whitespace() {
opts.push(opt);
}
}
for &child_id in &node.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::ValueLine {
for opt in child.content_span.slice(source).split_whitespace() {
opts.push(opt);
}
}
}
opts
}
fn parse_build_depends<'a>(cst: &'a CabalCst, node_id: NodeId) -> Vec<Dependency<'a>> {
let node = cst.node(node_id);
let source = cst.source.as_str();
let mut deps = Vec::new();
if let Some(ref val_span) = node.field_value {
let text = val_span.slice(source).trim();
deps.extend(parse_dependencies_from_text(text, node_id));
}
for &child_id in &node.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::ValueLine {
let text = child.content_span.slice(source).trim();
if !text.is_empty() {
deps.extend(parse_dependencies_from_text(text, child_id));
}
}
}
deps
}
fn derive_top_level_field<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
let node = cst.node(node_id);
let source = cst.source.as_str();
let raw_name = match node.field_name {
Some(ref span) => span.slice(source),
None => return,
};
let canon = canonicalize_field_name(raw_name);
match canon.as_str() {
"cabal-version" => {
let raw = field_first_line_value(cst, node_id).unwrap_or("");
let version_str = raw.strip_prefix(">=").unwrap_or(raw).trim();
file.cabal_version = Some(CabalVersion {
raw,
version: Version::parse(version_str),
cst_node: node_id,
});
}
"name" => {
file.name = field_first_line_value(cst, node_id);
}
"version" => {
let raw = field_first_line_value(cst, node_id).unwrap_or("");
file.version = Version::parse(raw);
}
"license" => {
file.license = field_first_line_value(cst, node_id);
}
"synopsis" => {
file.synopsis = field_first_line_value(cst, node_id);
}
"description" => {
file.description = field_first_line_value(cst, node_id);
}
"author" => {
file.author = field_first_line_value(cst, node_id);
}
"maintainer" => {
file.maintainer = field_first_line_value(cst, node_id);
}
"homepage" => {
file.homepage = field_first_line_value(cst, node_id);
}
"bug-reports" => {
file.bug_reports = field_first_line_value(cst, node_id);
}
"category" => {
file.category = field_first_line_value(cst, node_id);
}
"build-type" => {
file.build_type = field_first_line_value(cst, node_id);
}
"tested-with" => {
file.tested_with = field_first_line_value(cst, node_id);
}
"extra-source-files" | "extra-doc-files" => {
file.extra_source_files
.extend(parse_list_field(cst, node_id));
}
_ => {
let value = field_full_value(cst, node_id);
file.other_fields.push(Field {
name: canon,
raw_name,
value,
cst_node: node_id,
});
}
}
}
fn derive_section<'a>(cst: &'a CabalCst, node_id: NodeId, file: &mut CabalFile<'a>) {
let node = cst.node(node_id);
let source = cst.source.as_str();
let keyword = match node.section_keyword {
Some(ref span) => span.slice(source),
None => return,
};
let section_arg = node.section_arg.map(|span| span.slice(source));
let keyword_lower = keyword.to_ascii_lowercase();
match keyword_lower.as_str() {
"library" => {
let lib = derive_library(cst, node_id, section_arg);
if section_arg.is_some() {
file.named_libraries.push(lib);
} else {
file.library = Some(lib);
}
}
"executable" => {
let exe = derive_executable(cst, node_id, section_arg);
file.executables.push(exe);
}
"test-suite" => {
let ts = derive_test_suite(cst, node_id, section_arg);
file.test_suites.push(ts);
}
"benchmark" => {
let bm = derive_benchmark(cst, node_id, section_arg);
file.benchmarks.push(bm);
}
"common" => {
if let Some(name) = section_arg {
let cs = derive_common_stanza(cst, node_id, name);
file.common_stanzas.push(cs);
}
}
"flag" => {
if let Some(name) = section_arg {
let flag = derive_flag(cst, node_id, name);
file.flags.push(flag);
}
}
"source-repository" => {
let sr = derive_source_repository(cst, node_id, section_arg);
file.source_repositories.push(sr);
}
_ => {
}
}
}
fn empty_component_fields<'a>(name: Option<&'a str>, cst_node: NodeId) -> ComponentFields<'a> {
ComponentFields {
name,
cst_node,
imports: Vec::new(),
build_depends: Vec::new(),
other_modules: Vec::new(),
hs_source_dirs: Vec::new(),
default_language: None,
default_extensions: Vec::new(),
ghc_options: Vec::new(),
other_fields: Vec::new(),
conditionals: Vec::new(),
}
}
fn populate_component_fields<'a>(
cst: &'a CabalCst,
section_id: NodeId,
fields: &mut ComponentFields<'a>,
) {
let section = cst.node(section_id);
let source = cst.source.as_str();
for &child_id in §ion.children {
let child = cst.node(child_id);
match child.kind {
CstNodeKind::Field => {
let raw_name = match child.field_name {
Some(ref span) => span.slice(source),
None => continue,
};
let canon = canonicalize_field_name(raw_name);
match canon.as_str() {
"build-depends" => {
fields
.build_depends
.extend(parse_build_depends(cst, child_id));
}
"exposed-modules" => {
}
"other-modules" => {
fields.other_modules.extend(parse_list_field(cst, child_id));
}
"hs-source-dirs" => {
fields
.hs_source_dirs
.extend(parse_list_field(cst, child_id));
}
"default-language" => {
fields.default_language = field_first_line_value(cst, child_id);
}
"default-extensions" | "extensions" => {
fields
.default_extensions
.extend(parse_list_field(cst, child_id));
}
"ghc-options" => {
fields.ghc_options.extend(parse_ghc_options(cst, child_id));
}
_ => {
let value = field_full_value(cst, child_id);
fields.other_fields.push(Field {
name: canon,
raw_name,
value,
cst_node: child_id,
});
}
}
}
CstNodeKind::Import => {
if let Some(ref val_span) = child.field_value {
let val = val_span.slice(source).trim();
if !val.is_empty() {
for item in val.split(',') {
let item = item.trim();
if !item.is_empty() {
fields.imports.push(item);
}
}
}
}
}
CstNodeKind::Conditional => {
let cond = derive_conditional(cst, child_id);
fields.conditionals.push(cond);
}
_ => {}
}
}
}
fn derive_conditional<'a>(cst: &'a CabalCst, node_id: NodeId) -> Conditional<'a> {
let node = cst.node(node_id);
let source = cst.source.as_str();
let condition = match node.condition_expr {
Some(ref span) => parse_condition(span.slice(source)),
None => Condition::Raw(""),
};
let mut cond = Conditional {
condition,
then_fields: Vec::new(),
then_deps: Vec::new(),
else_fields: Vec::new(),
else_deps: Vec::new(),
then_conditionals: Vec::new(),
else_conditionals: Vec::new(),
cst_node: node_id,
};
for &child_id in &node.children {
let child = cst.node(child_id);
match child.kind {
CstNodeKind::Field => {
let raw_name = match child.field_name {
Some(ref span) => span.slice(source),
None => continue,
};
let canon = canonicalize_field_name(raw_name);
if canon == "build-depends" {
cond.then_deps.extend(parse_build_depends(cst, child_id));
} else {
let value = field_full_value(cst, child_id);
cond.then_fields.push(Field {
name: canon,
raw_name,
value,
cst_node: child_id,
});
}
}
CstNodeKind::Conditional => {
cond.then_conditionals
.push(derive_conditional(cst, child_id));
}
CstNodeKind::ElseBlock => {
for &else_child_id in &child.children {
let else_child = cst.node(else_child_id);
match else_child.kind {
CstNodeKind::Field => {
let raw_name = match else_child.field_name {
Some(ref span) => span.slice(source),
None => continue,
};
let canon = canonicalize_field_name(raw_name);
if canon == "build-depends" {
cond.else_deps
.extend(parse_build_depends(cst, else_child_id));
} else {
let value = field_full_value(cst, else_child_id);
cond.else_fields.push(Field {
name: canon,
raw_name,
value,
cst_node: else_child_id,
});
}
}
CstNodeKind::Conditional => {
cond.else_conditionals
.push(derive_conditional(cst, else_child_id));
}
_ => {}
}
}
}
_ => {}
}
}
cond
}
fn derive_library<'a>(cst: &'a CabalCst, node_id: NodeId, name: Option<&'a str>) -> Library<'a> {
let mut fields = empty_component_fields(name, node_id);
populate_component_fields(cst, node_id, &mut fields);
let exposed_modules = extract_exposed_modules(cst, node_id);
Library {
fields,
exposed_modules,
}
}
fn extract_exposed_modules(cst: &CabalCst, section_id: NodeId) -> Vec<&str> {
let section = cst.node(section_id);
let source = cst.source.as_str();
let mut modules = Vec::new();
for &child_id in §ion.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::Field {
if let Some(ref name_span) = child.field_name {
let canon = canonicalize_field_name(name_span.slice(source));
if canon == "exposed-modules" {
modules.extend(parse_list_field(cst, child_id));
}
}
}
}
modules
}
fn derive_executable<'a>(
cst: &'a CabalCst,
node_id: NodeId,
name: Option<&'a str>,
) -> Executable<'a> {
let main_is = find_field_value_in_section(cst, node_id, "main-is");
let mut fields = empty_component_fields(name, node_id);
populate_component_fields(cst, node_id, &mut fields);
remove_field_by_name(&mut fields.other_fields, "main-is");
Executable { fields, main_is }
}
fn derive_test_suite<'a>(
cst: &'a CabalCst,
node_id: NodeId,
name: Option<&'a str>,
) -> TestSuite<'a> {
let test_type = find_field_value_in_section(cst, node_id, "type");
let main_is = find_field_value_in_section(cst, node_id, "main-is");
let mut fields = empty_component_fields(name, node_id);
populate_component_fields(cst, node_id, &mut fields);
remove_field_by_name(&mut fields.other_fields, "type");
remove_field_by_name(&mut fields.other_fields, "main-is");
TestSuite {
fields,
test_type,
main_is,
}
}
fn derive_benchmark<'a>(
cst: &'a CabalCst,
node_id: NodeId,
name: Option<&'a str>,
) -> Benchmark<'a> {
let bench_type = find_field_value_in_section(cst, node_id, "type");
let main_is = find_field_value_in_section(cst, node_id, "main-is");
let mut fields = empty_component_fields(name, node_id);
populate_component_fields(cst, node_id, &mut fields);
remove_field_by_name(&mut fields.other_fields, "type");
remove_field_by_name(&mut fields.other_fields, "main-is");
Benchmark {
fields,
bench_type,
main_is,
}
}
fn derive_common_stanza<'a>(cst: &'a CabalCst, node_id: NodeId, name: &'a str) -> CommonStanza<'a> {
let mut fields = empty_component_fields(Some(name), node_id);
populate_component_fields(cst, node_id, &mut fields);
CommonStanza { name, fields }
}
fn derive_flag<'a>(cst: &'a CabalCst, node_id: NodeId, name: &'a str) -> Flag<'a> {
let section = cst.node(node_id);
let source = cst.source.as_str();
let mut description = None;
let mut default = None;
let mut manual = None;
let mut other_fields = Vec::new();
for &child_id in §ion.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::Field {
let raw_name = match child.field_name {
Some(ref span) => span.slice(source),
None => continue,
};
let canon = canonicalize_field_name(raw_name);
match canon.as_str() {
"description" => {
description = field_first_line_value(cst, child_id);
}
"default" => {
if let Some(val) = field_first_line_value(cst, child_id) {
let lower = val.to_ascii_lowercase();
default = Some(lower == "true");
}
}
"manual" => {
if let Some(val) = field_first_line_value(cst, child_id) {
let lower = val.to_ascii_lowercase();
manual = Some(lower == "true");
}
}
_ => {
let value = field_full_value(cst, child_id);
other_fields.push(Field {
name: canon,
raw_name,
value,
cst_node: child_id,
});
}
}
}
}
Flag {
name,
description,
default,
manual,
other_fields,
cst_node: node_id,
}
}
fn derive_source_repository<'a>(
cst: &'a CabalCst,
node_id: NodeId,
kind: Option<&'a str>,
) -> SourceRepository<'a> {
let section = cst.node(node_id);
let source = cst.source.as_str();
let mut repo_type = None;
let mut location = None;
let mut tag = None;
let mut branch = None;
let mut subdir = None;
let mut other_fields = Vec::new();
for &child_id in §ion.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::Field {
let raw_name = match child.field_name {
Some(ref span) => span.slice(source),
None => continue,
};
let canon = canonicalize_field_name(raw_name);
match canon.as_str() {
"type" => {
repo_type = field_first_line_value(cst, child_id);
}
"location" => {
location = field_first_line_value(cst, child_id);
}
"tag" => {
tag = field_first_line_value(cst, child_id);
}
"branch" => {
branch = field_first_line_value(cst, child_id);
}
"subdir" => {
subdir = field_first_line_value(cst, child_id);
}
_ => {
let value = field_full_value(cst, child_id);
other_fields.push(Field {
name: canon,
raw_name,
value,
cst_node: child_id,
});
}
}
}
}
SourceRepository {
kind,
repo_type,
location,
tag,
branch,
subdir,
other_fields,
cst_node: node_id,
}
}
fn remove_field_by_name(fields: &mut Vec<Field<'_>>, canonical_name: &str) {
fields.retain(|f| f.name != canonical_name);
}
fn find_field_value_in_section<'a>(
cst: &'a CabalCst,
section_id: NodeId,
target_canon: &str,
) -> Option<&'a str> {
let section = cst.node(section_id);
let source = cst.source.as_str();
for &child_id in §ion.children {
let child = cst.node(child_id);
if child.kind == CstNodeKind::Field {
if let Some(ref name_span) = child.field_name {
let canon = canonicalize_field_name(name_span.slice(source));
if canon == target_canon {
return field_first_line_value(cst, child_id);
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn do_parse(source: &str) -> crate::parse::ParseResult {
crate::parse::parse(source)
}
#[test]
fn version_parse_simple() {
let v = Version::parse("0.1.0.0").unwrap();
assert_eq!(v.components, vec![0, 1, 0, 0]);
}
#[test]
fn version_parse_two_components() {
let v = Version::parse("4.14").unwrap();
assert_eq!(v.components, vec![4, 14]);
}
#[test]
fn version_parse_single() {
let v = Version::parse("5").unwrap();
assert_eq!(v.components, vec![5]);
}
#[test]
fn version_parse_empty() {
assert!(Version::parse("").is_none());
}
#[test]
fn version_parse_invalid() {
assert!(Version::parse("abc").is_none());
assert!(Version::parse("1.2.abc").is_none());
}
#[test]
fn version_display() {
let v = Version {
components: vec![1, 2, 3, 0],
};
assert_eq!(v.to_string(), "1.2.3.0");
}
#[test]
fn version_range_gte() {
let vr = parse_version_range(">=4.14").unwrap();
assert_eq!(
vr,
VersionRange::Gte(Version {
components: vec![4, 14]
})
);
}
#[test]
fn version_range_lt() {
let vr = parse_version_range("<5").unwrap();
assert_eq!(
vr,
VersionRange::Lt(Version {
components: vec![5]
})
);
}
#[test]
fn version_range_major_bound() {
let vr = parse_version_range("^>=2.2").unwrap();
assert_eq!(
vr,
VersionRange::MajorBound(Version {
components: vec![2, 2]
})
);
}
#[test]
fn version_range_eq() {
let vr = parse_version_range("==1.0").unwrap();
assert_eq!(
vr,
VersionRange::Eq(Version {
components: vec![1, 0]
})
);
}
#[test]
fn version_range_and() {
let vr = parse_version_range(">=4.14 && <5").unwrap();
assert_eq!(
vr,
VersionRange::And(
Box::new(VersionRange::Gte(Version {
components: vec![4, 14]
})),
Box::new(VersionRange::Lt(Version {
components: vec![5]
})),
)
);
}
#[test]
fn version_range_or() {
let vr = parse_version_range(">=2.0 || ==1.9").unwrap();
assert_eq!(
vr,
VersionRange::Or(
Box::new(VersionRange::Gte(Version {
components: vec![2, 0]
})),
Box::new(VersionRange::Eq(Version {
components: vec![1, 9]
})),
)
);
}
#[test]
fn version_range_complex_and() {
let vr = parse_version_range(">=2.0 && <2.2").unwrap();
assert_eq!(
vr,
VersionRange::And(
Box::new(VersionRange::Gte(Version {
components: vec![2, 0]
})),
Box::new(VersionRange::Lt(Version {
components: vec![2, 2]
})),
)
);
}
#[test]
fn version_range_empty() {
assert!(parse_version_range("").is_none());
}
#[test]
fn canonicalize_mixed_case() {
assert_eq!(canonicalize_field_name("Build-Depends"), "build-depends");
}
#[test]
fn canonicalize_underscore() {
assert_eq!(canonicalize_field_name("build_depends"), "build-depends");
}
#[test]
fn canonicalize_already_canonical() {
assert_eq!(canonicalize_field_name("build-depends"), "build-depends");
}
#[test]
fn parse_dep_no_version() {
let dep = parse_single_dependency("base", NodeId(0)).unwrap();
assert_eq!(dep.package, "base");
assert!(dep.version_range.is_none());
}
#[test]
fn parse_dep_with_version() {
let dep = parse_single_dependency("aeson ^>=2.2", NodeId(0)).unwrap();
assert_eq!(dep.package, "aeson");
assert_eq!(
dep.version_range,
Some(VersionRange::MajorBound(Version {
components: vec![2, 2]
}))
);
}
#[test]
fn parse_dep_with_range() {
let dep = parse_single_dependency("base >=4.14 && <5", NodeId(0)).unwrap();
assert_eq!(dep.package, "base");
assert_eq!(
dep.version_range,
Some(VersionRange::And(
Box::new(VersionRange::Gte(Version {
components: vec![4, 14]
})),
Box::new(VersionRange::Lt(Version {
components: vec![5]
})),
))
);
}
#[test]
fn parse_deps_comma_separated() {
let deps = parse_dependencies_from_text("base >=4.14, text >=2.0, aeson ^>=2.2", NodeId(0));
assert_eq!(deps.len(), 3);
assert_eq!(deps[0].package, "base");
assert_eq!(deps[1].package, "text");
assert_eq!(deps[2].package, "aeson");
}
#[test]
fn parse_deps_empty() {
let deps = parse_dependencies_from_text("", NodeId(0));
assert!(deps.is_empty());
}
#[test]
fn parse_condition_flag() {
let c = parse_condition("flag(dev)");
assert_eq!(c, Condition::Flag("dev"));
}
#[test]
fn parse_condition_os() {
let c = parse_condition("os(windows)");
assert_eq!(c, Condition::OS("windows"));
}
#[test]
fn parse_condition_arch() {
let c = parse_condition("arch(x86_64)");
assert_eq!(c, Condition::Arch("x86_64"));
}
#[test]
fn parse_condition_impl() {
let c = parse_condition("impl(ghc >= 9.6)");
assert_eq!(
c,
Condition::Impl(
"ghc",
Some(VersionRange::Gte(Version {
components: vec![9, 6]
}))
)
);
}
#[test]
fn parse_condition_not() {
let c = parse_condition("!os(windows)");
assert_eq!(c, Condition::Not(Box::new(Condition::OS("windows"))));
}
#[test]
fn parse_condition_and() {
let c = parse_condition("flag(dev) && !os(windows)");
assert_eq!(
c,
Condition::And(
Box::new(Condition::Flag("dev")),
Box::new(Condition::Not(Box::new(Condition::OS("windows")))),
)
);
}
#[test]
fn parse_condition_or() {
let c = parse_condition("flag(a) || flag(b)");
assert_eq!(
c,
Condition::Or(
Box::new(Condition::Flag("a")),
Box::new(Condition::Flag("b")),
)
);
}
#[test]
fn parse_condition_empty() {
let c = parse_condition("");
assert_eq!(c, Condition::Raw(""));
}
#[test]
fn derive_minimal_file() {
let src = "cabal-version: 3.0\nname: my-pkg\nversion: 0.1.0.0\n";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.name, Some("my-pkg"));
assert_eq!(
ast.version,
Some(Version {
components: vec![0, 1, 0, 0]
})
);
assert!(ast.cabal_version.is_some());
let cv = ast.cabal_version.as_ref().unwrap();
assert_eq!(cv.raw, "3.0");
assert_eq!(
cv.version,
Some(Version {
components: vec![3, 0]
})
);
}
#[test]
fn derive_with_library() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
exposed-modules:
Foo
Bar
build-depends:
base >=4.14
default-language: GHC2021
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert!(ast.library.is_some());
let lib = ast.library.as_ref().unwrap();
assert_eq!(lib.exposed_modules, vec!["Foo", "Bar"]);
assert_eq!(lib.fields.build_depends.len(), 1);
assert_eq!(lib.fields.build_depends[0].package, "base");
assert_eq!(lib.fields.default_language, Some("GHC2021"));
}
#[test]
fn derive_with_executable() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
executable my-exe
main-is: Main.hs
build-depends: base
hs-source-dirs: app
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.executables.len(), 1);
let exe = &ast.executables[0];
assert_eq!(exe.fields.name, Some("my-exe"));
assert_eq!(exe.main_is, Some("Main.hs"));
assert_eq!(exe.fields.build_depends.len(), 1);
assert_eq!(exe.fields.hs_source_dirs, vec!["app"]);
}
#[test]
fn derive_with_test_suite() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
test-suite my-tests
type: exitcode-stdio-1.0
main-is: Main.hs
build-depends: base, tasty
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.test_suites.len(), 1);
let ts = &ast.test_suites[0];
assert_eq!(ts.fields.name, Some("my-tests"));
assert_eq!(ts.test_type, Some("exitcode-stdio-1.0"));
assert_eq!(ts.main_is, Some("Main.hs"));
assert_eq!(ts.fields.build_depends.len(), 2);
}
#[test]
fn derive_with_common_stanza() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
common warnings
ghc-options: -Wall -Wcompat
library
import: warnings
exposed-modules: Foo
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.common_stanzas.len(), 1);
assert_eq!(ast.common_stanzas[0].name, "warnings");
assert_eq!(
ast.common_stanzas[0].fields.ghc_options,
vec!["-Wall", "-Wcompat"]
);
let lib = ast.library.as_ref().unwrap();
assert_eq!(lib.fields.imports, vec!["warnings"]);
}
#[test]
fn derive_with_flag() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
flag dev
description: Development mode
default: False
manual: True
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.flags.len(), 1);
let flag = &ast.flags[0];
assert_eq!(flag.name, "dev");
assert_eq!(flag.description, Some("Development mode"));
assert_eq!(flag.default, Some(false));
assert_eq!(flag.manual, Some(true));
}
#[test]
fn derive_with_source_repository() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
source-repository head
type: git
location: https://github.com/example/my-pkg
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.source_repositories.len(), 1);
let sr = &ast.source_repositories[0];
assert_eq!(sr.kind, Some("head"));
assert_eq!(sr.repo_type, Some("git"));
assert_eq!(sr.location, Some("https://github.com/example/my-pkg"));
}
#[test]
fn derive_conditional() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends: base
if flag(dev)
ghc-options: -O0
else
ghc-options: -O2
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let lib = ast.library.as_ref().unwrap();
assert_eq!(lib.fields.conditionals.len(), 1);
let cond = &lib.fields.conditionals[0];
assert_eq!(cond.condition, Condition::Flag("dev"));
assert_eq!(cond.then_fields.len(), 1);
assert_eq!(cond.then_fields[0].name, "ghc-options");
assert_eq!(cond.then_fields[0].value, "-O0");
assert_eq!(cond.else_fields.len(), 1);
assert_eq!(cond.else_fields[0].name, "ghc-options");
assert_eq!(cond.else_fields[0].value, "-O2");
}
#[test]
fn derive_all_dependencies() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends: base, text
executable my-exe
build-depends: base, my-pkg
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let all_deps = ast.all_dependencies();
assert_eq!(all_deps.len(), 4);
let names: Vec<&str> = all_deps.iter().map(|d| d.package).collect();
assert!(names.contains(&"base"));
assert!(names.contains(&"text"));
assert!(names.contains(&"my-pkg"));
}
#[test]
fn derive_all_components() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
exposed-modules: Foo
executable my-exe
main-is: Main.hs
test-suite my-tests
type: exitcode-stdio-1.0
main-is: Main.hs
benchmark my-bench
type: exitcode-stdio-1.0
main-is: Main.hs
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let comps = ast.all_components();
assert_eq!(comps.len(), 4);
}
#[test]
fn derive_find_component() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
exposed-modules: Foo
executable my-exe
main-is: Main.hs
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert!(ast.find_component("library").is_some());
assert!(ast.find_component("my-exe").is_some());
assert!(ast.find_component("nonexistent").is_none());
}
#[test]
fn derive_cst_node_back_references_valid() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends: base >=4.14
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.cst_root, result.cst.root);
let lib = ast.library.as_ref().unwrap();
let node = result.cst.node(lib.fields.cst_node);
assert_eq!(node.kind, CstNodeKind::Section);
assert!(!lib.fields.build_depends.is_empty());
let dep_node_id = lib.fields.build_depends[0].cst_node;
assert!(dep_node_id.0 < result.cst.node_count());
}
#[test]
fn derive_deps_leading_comma_style() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends:
base >=4.14
, text >=2.0
, aeson ^>=2.2
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let lib = ast.library.as_ref().unwrap();
assert_eq!(lib.fields.build_depends.len(), 3);
assert_eq!(lib.fields.build_depends[0].package, "base");
assert_eq!(lib.fields.build_depends[1].package, "text");
assert_eq!(lib.fields.build_depends[2].package, "aeson");
}
#[test]
fn derive_deps_trailing_comma_style() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends:
base >=4.14,
text >=2.0,
aeson ^>=2.2
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let lib = ast.library.as_ref().unwrap();
assert_eq!(lib.fields.build_depends.len(), 3);
assert_eq!(lib.fields.build_depends[0].package, "base");
assert_eq!(lib.fields.build_depends[1].package, "text");
assert_eq!(lib.fields.build_depends[2].package, "aeson");
}
#[test]
fn derive_deps_single_line() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends: base >=4.14, text >=2.0, aeson ^>=2.2
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let lib = ast.library.as_ref().unwrap();
assert_eq!(lib.fields.build_depends.len(), 3);
}
#[test]
fn derive_default_extensions() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
default-extensions:
OverloadedStrings
DerivingStrategies
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let lib = ast.library.as_ref().unwrap();
assert_eq!(
lib.fields.default_extensions,
vec!["OverloadedStrings", "DerivingStrategies"]
);
}
#[test]
fn derive_metadata_fields() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
license: MIT
synopsis: A test package
author: Test Author
maintainer: test@example.com
homepage: https://example.com
bug-reports: https://example.com/issues
category: Development
build-type: Simple
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.license, Some("MIT"));
assert_eq!(ast.synopsis, Some("A test package"));
assert_eq!(ast.author, Some("Test Author"));
assert_eq!(ast.maintainer, Some("test@example.com"));
assert_eq!(ast.homepage, Some("https://example.com"));
assert_eq!(ast.bug_reports, Some("https://example.com/issues"));
assert_eq!(ast.category, Some("Development"));
assert_eq!(ast.build_type, Some("Simple"));
}
#[test]
fn derive_conditional_deps() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
library
build-depends: base
if os(windows)
build-depends: Win32
else
build-depends: unix
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
let all_deps = ast.all_dependencies();
let names: Vec<&str> = all_deps.iter().map(|d| d.package).collect();
assert!(names.contains(&"base"));
assert!(names.contains(&"Win32"));
assert!(names.contains(&"unix"));
assert_eq!(all_deps.len(), 3);
}
#[test]
fn parse_condition_true() {
assert_eq!(parse_condition("true"), Condition::Lit(true));
}
#[test]
fn parse_condition_false() {
assert_eq!(parse_condition("false"), Condition::Lit(false));
}
#[test]
fn parse_condition_true_case_insensitive() {
assert_eq!(parse_condition("True"), Condition::Lit(true));
assert_eq!(parse_condition("FALSE"), Condition::Lit(false));
}
#[test]
fn version_range_wildcard() {
let r = parse_version_range("==1.2.*").unwrap();
match r {
VersionRange::And(a, b) => {
assert_eq!(
*a,
VersionRange::Gte(Version {
components: vec![1, 2]
})
);
assert_eq!(
*b,
VersionRange::Lt(Version {
components: vec![1, 3]
})
);
}
_ => panic!("Expected And range, got {:?}", r),
}
}
#[test]
fn version_range_any_keyword() {
assert_eq!(parse_version_range("-any").unwrap(), VersionRange::Any);
}
#[test]
fn version_range_none_keyword() {
assert_eq!(
parse_version_range("-none").unwrap(),
VersionRange::NoVersion
);
}
#[test]
fn version_range_set_major_bound() {
let r = parse_version_range("^>= { 2.6, 2.7, 2.8 }").unwrap();
match r {
VersionRange::Or(_, _) => {} _ => panic!("Expected Or range for set notation, got {:?}", r),
}
}
#[test]
fn version_range_set_eq() {
let r = parse_version_range("== { 1.0, 2.0 }").unwrap();
match r {
VersionRange::Or(_, _) => {}
_ => panic!("Expected Or range for set notation, got {:?}", r),
}
}
#[test]
fn version_range_display_any() {
assert_eq!(VersionRange::Any.to_string(), "-any");
}
#[test]
fn version_range_display_none() {
assert_eq!(VersionRange::NoVersion.to_string(), "-none");
}
#[test]
fn derive_benchmark() {
let src = "\
cabal-version: 3.0
name: my-pkg
version: 0.1.0.0
benchmark my-bench
type: exitcode-stdio-1.0
main-is: Main.hs
build-depends: base, criterion
hs-source-dirs: bench
";
let result = do_parse(src);
let ast = derive_ast(&result.cst);
assert_eq!(ast.benchmarks.len(), 1);
let bm = &ast.benchmarks[0];
assert_eq!(bm.fields.name, Some("my-bench"));
assert_eq!(bm.bench_type, Some("exitcode-stdio-1.0"));
assert_eq!(bm.main_is, Some("Main.hs"));
assert_eq!(bm.fields.build_depends.len(), 2);
}
}