cfgcomment-core 0.3.0

Core logic of cfgcomment
Documentation
use std::{
    cell::RefCell,
    collections::{HashMap, HashSet},
    fs::File,
    io::{BufRead, BufReader, BufWriter, Write},
    path::PathBuf,
    rc::Rc,
    sync::Arc,
};

pub struct Data {
    pub features: HashSet<String>,
    pub reset: bool,
}
impl Data {
    fn has_feature(&self, feature: &str) -> bool {
        self.features.contains(feature)
    }
}

enum Predicate {
    Feature(String),
}
impl Predicate {
    fn matches(&self, config: &Data) -> bool {
        match self {
            Self::Feature(f) => config.has_feature(f),
        }
    }
}

enum Group {
    Option(Predicate),
    All(Vec<Self>),
    Any(Vec<Self>),
    Not(Box<Self>),
}

impl Group {
    fn matches(&self, config: &Data) -> bool {
        match self {
            Self::Option(o) => o.matches(config),
            Self::All(v) => v.iter().all(|p| p.matches(config)),
            Self::Any(v) => v.iter().any(|p| p.matches(config)),
            Self::Not(v) => !v.matches(config),
        }
    }
}

enum CfgTag {
    Start(Group),
    End,
}

peg::parser! {
    grammar cfg() for str {
        pub(crate) rule cfg() -> CfgTag
            = "[" _ "cfg" _ "(" _ "end" _ ")" _ "]" {CfgTag::End}
            / "[" _ "cfg" _ "(" _ p:pred() _ ")" _ "]" {CfgTag::Start(p)}

        rule opt() -> Predicate
            = "feature" _ "=" _ "\"" s:$((!['"'] [_])*) "\"" {Predicate::Feature(s.to_owned())}

        rule pred() -> Group
            = "any" _ "(" _ l:pred_list() _ ")" {Group::Any(l)}
            / "all" _ "(" _ l:pred_list() _ ")" {Group::All(l)}
            / "not" _ "(" _ p:pred() _ ")" {Group::Not(Box::new(p))}
            / o:opt() {Group::Option(o)}

        rule list_sep() = _ "," _
        rule pred_list() -> Vec<Group>
            = l:pred()**list_sep() list_sep()? {l}

            rule _ = [' ' | '\t']*
    }
}

fn split_at_ws_end(i: &str) -> (&str, &str) {
    let idx = i
        .bytes()
        .position(|c| !c.is_ascii_whitespace())
        .unwrap_or(i.len());
    i.split_at(idx)
}

#[derive(Default, Clone)]
struct CfgState(Rc<RefCell<Vec<(bool, String)>>>);
impl CfgState {
    fn enabled(&self) -> bool {
        self.0.borrow().iter().all(|(p, _)| *p)
    }
    fn prefix(&self) -> String {
        self.0
            .borrow()
            .iter()
            .last()
            .map(|(_, p)| p.to_owned())
            .unwrap_or_else(|| "".to_owned())
    }
    fn push(&self, state: (bool, String)) {
        self.0.borrow_mut().push(state)
    }
    fn pop(&self) -> Option<()> {
        self.0.borrow_mut().pop().map(|_| ())
    }
}

#[derive(Clone)]
pub struct LangDesc {
    pub cfg_prefix: String,
    pub cfg_prefix_comment_len: usize,
    pub cfg_suffix: String,
    pub comment: String,
}

impl LangDesc {
    pub fn default_list() -> HashMap<String, Self> {
        let c_like = LangDesc {
            cfg_prefix: "//[".to_owned(),
            cfg_prefix_comment_len: 2,
            cfg_suffix: "]".to_owned(),
            comment: "//# ".to_owned(),
        };
        std::array::IntoIter::new([
            (
                "rs".to_owned(),
                c_like.clone(),
            ),
            (
                "js".to_owned(),
                c_like.clone(),
            ),
            (
                "ts".to_owned(),
                c_like.clone(),
            ),
            (
                "toml".to_owned(),
                LangDesc {
                    cfg_prefix: "#[".to_owned(),
                    cfg_prefix_comment_len: 1,
                    cfg_suffix: "]".to_owned(),
                    comment: "#- ".to_owned(),
                },
            ),
        ])
        .collect()
    }
}

pub fn process(
    read: impl Iterator<Item = String>,
    config: Arc<Data>,
    desc: Rc<LangDesc>,
) -> impl Iterator<Item = String> {
    let state = CfgState::default();
    read.map(move |s| {
        let state = state.clone();
        if s.trim_start().starts_with(&desc.cfg_prefix) && s.trim_end().ends_with(&desc.cfg_suffix)
        {
            let (ws, cfg) = split_at_ws_end(&s);
            let parsed = cfg::cfg(&cfg[desc.cfg_prefix_comment_len..]).unwrap();
            match parsed {
                CfgTag::Start(c) => {
                    state.push((c.matches(&config), ws.to_owned()));
                    s
                }
                CfgTag::End => {
                    state.pop().expect("unexpected end");
                    s
                }
            }
        } else {
            if s.trim().is_empty() {
                return s;
            }
            let prefix = state.prefix();
            let trimmed = s.strip_prefix(&prefix).expect("wrong prefix");
            let enabled = !trimmed.starts_with(&desc.comment);
            let should_be = config.reset || state.enabled();

            log::trace!("{} {:?} {:?}", trimmed, enabled, should_be);
            if !enabled && should_be {
                format!("{}{}", prefix, &trimmed[desc.comment.len()..])
            } else if enabled && !should_be {
                format!("{}{}{}", prefix, desc.comment, trimmed)
            } else {
                s
            }
        }
    })
}

pub fn walkdir_parallel(paths: Vec<PathBuf>, config: Data, lang_config: HashMap<String, LangDesc>) {
    let mut walk = ignore::WalkBuilder::new(paths[0].clone());
    for dir in paths.iter().skip(1) {
        walk.add(dir);
    }
    walk.add_custom_ignore_filename(".cfgignore");

    let config = Arc::new(config);
    let lang_config = Arc::new(lang_config);

    walk.build_parallel().run(move || {
        let config = config.clone();
        let lang_config = lang_config.clone();
        Box::new(move |path| {
            let path = path.unwrap();
            // Skip dirs/symlinks
            if !path.file_type().map(|f| f.is_file()).unwrap_or(false) {
                return ignore::WalkState::Continue;
            }
            let extension = match path.path().extension() {
                Some(v) => v,
                None => return ignore::WalkState::Continue,
            };
            let extension = extension.to_string_lossy().to_string();
            let desc = match lang_config.get(&extension) {
                Some(v) => v,
                None => return ignore::WalkState::Continue,
            };
            let desc = Rc::new(desc.clone());

            let file = BufReader::new(File::open(path.path()).unwrap());
            let mut out = BufWriter::new(
                tempfile::NamedTempFile::new_in(path.path().parent().unwrap()).unwrap(),
            );

            for line in process(file.lines().map(|l| l.unwrap()), config.clone(), desc) {
                writeln!(out, "{}", line).unwrap();
            }

            out.into_inner().unwrap().persist(path.path()).unwrap();

            ignore::WalkState::Continue
        })
    });
}