cfgcomment_core/
lib.rs

1use std::{
2    cell::RefCell,
3    collections::{HashMap, HashSet},
4    fs::File,
5    io::{BufRead, BufReader, BufWriter, Write},
6    path::PathBuf,
7    rc::Rc,
8    sync::Arc,
9};
10
11pub struct Data {
12    pub features: HashSet<String>,
13    pub reset: bool,
14}
15impl Data {
16    fn has_feature(&self, feature: &str) -> bool {
17        self.features.contains(feature)
18    }
19}
20
21enum Predicate {
22    Feature(String),
23}
24impl Predicate {
25    fn matches(&self, config: &Data) -> bool {
26        match self {
27            Self::Feature(f) => config.has_feature(f),
28        }
29    }
30}
31
32enum Group {
33    Option(Predicate),
34    All(Vec<Self>),
35    Any(Vec<Self>),
36    Not(Box<Self>),
37}
38
39impl Group {
40    fn matches(&self, config: &Data) -> bool {
41        match self {
42            Self::Option(o) => o.matches(config),
43            Self::All(v) => v.iter().all(|p| p.matches(config)),
44            Self::Any(v) => v.iter().any(|p| p.matches(config)),
45            Self::Not(v) => !v.matches(config),
46        }
47    }
48}
49
50enum CfgTag {
51    Start(Group),
52    End,
53}
54
55peg::parser! {
56    grammar cfg() for str {
57        pub(crate) rule cfg() -> CfgTag
58            = "[" _ "cfg" _ "(" _ "end" _ ")" _ "]" {CfgTag::End}
59            / "[" _ "cfg" _ "(" _ p:pred() _ ")" _ "]" {CfgTag::Start(p)}
60
61        rule opt() -> Predicate
62            = "feature" _ "=" _ "\"" s:$((!['"'] [_])*) "\"" {Predicate::Feature(s.to_owned())}
63
64        rule pred() -> Group
65            = "any" _ "(" _ l:pred_list() _ ")" {Group::Any(l)}
66            / "all" _ "(" _ l:pred_list() _ ")" {Group::All(l)}
67            / "not" _ "(" _ p:pred() _ ")" {Group::Not(Box::new(p))}
68            / o:opt() {Group::Option(o)}
69
70        rule list_sep() = _ "," _
71        rule pred_list() -> Vec<Group>
72            = l:pred()**list_sep() list_sep()? {l}
73
74            rule _ = [' ' | '\t']*
75    }
76}
77
78fn split_at_ws_end(i: &str) -> (&str, &str) {
79    let idx = i
80        .bytes()
81        .position(|c| !c.is_ascii_whitespace())
82        .unwrap_or(i.len());
83    i.split_at(idx)
84}
85
86#[derive(Default, Clone)]
87struct CfgState(Rc<RefCell<Vec<(bool, String)>>>);
88impl CfgState {
89    fn enabled(&self) -> bool {
90        self.0.borrow().iter().all(|(p, _)| *p)
91    }
92    fn prefix(&self) -> String {
93        self.0
94            .borrow()
95            .iter()
96            .last()
97            .map(|(_, p)| p.to_owned())
98            .unwrap_or_else(|| "".to_owned())
99    }
100    fn push(&self, state: (bool, String)) {
101        self.0.borrow_mut().push(state)
102    }
103    fn pop(&self) -> Option<()> {
104        self.0.borrow_mut().pop().map(|_| ())
105    }
106}
107
108#[derive(Clone)]
109pub struct LangDesc {
110    pub cfg_prefix: String,
111    pub cfg_prefix_comment_len: usize,
112    pub cfg_suffix: String,
113    pub comment: String,
114}
115
116impl LangDesc {
117    pub fn default_list() -> HashMap<String, Self> {
118        let c_like = LangDesc {
119            cfg_prefix: "//[".to_owned(),
120            cfg_prefix_comment_len: 2,
121            cfg_suffix: "]".to_owned(),
122            comment: "//# ".to_owned(),
123        };
124        std::array::IntoIter::new([
125            (
126                "rs".to_owned(),
127                c_like.clone(),
128            ),
129            (
130                "js".to_owned(),
131                c_like.clone(),
132            ),
133            (
134                "ts".to_owned(),
135                c_like.clone(),
136            ),
137            (
138                "toml".to_owned(),
139                LangDesc {
140                    cfg_prefix: "#[".to_owned(),
141                    cfg_prefix_comment_len: 1,
142                    cfg_suffix: "]".to_owned(),
143                    comment: "#- ".to_owned(),
144                },
145            ),
146        ])
147        .collect()
148    }
149}
150
151pub fn process(
152    read: impl Iterator<Item = String>,
153    config: Arc<Data>,
154    desc: Rc<LangDesc>,
155) -> impl Iterator<Item = String> {
156    let state = CfgState::default();
157    read.map(move |s| {
158        let state = state.clone();
159        if s.trim_start().starts_with(&desc.cfg_prefix) && s.trim_end().ends_with(&desc.cfg_suffix)
160        {
161            let (ws, cfg) = split_at_ws_end(&s);
162            let parsed = cfg::cfg(&cfg[desc.cfg_prefix_comment_len..]).unwrap();
163            match parsed {
164                CfgTag::Start(c) => {
165                    state.push((c.matches(&config), ws.to_owned()));
166                    s
167                }
168                CfgTag::End => {
169                    state.pop().expect("unexpected end");
170                    s
171                }
172            }
173        } else {
174            if s.trim().is_empty() {
175                return s;
176            }
177            let prefix = state.prefix();
178            let trimmed = s.strip_prefix(&prefix).expect("wrong prefix");
179            let enabled = !trimmed.starts_with(&desc.comment);
180            let should_be = config.reset || state.enabled();
181
182            log::trace!("{} {:?} {:?}", trimmed, enabled, should_be);
183            if !enabled && should_be {
184                format!("{}{}", prefix, &trimmed[desc.comment.len()..])
185            } else if enabled && !should_be {
186                format!("{}{}{}", prefix, desc.comment, trimmed)
187            } else {
188                s
189            }
190        }
191    })
192}
193
194pub fn walkdir_parallel(paths: Vec<PathBuf>, config: Data, lang_config: HashMap<String, LangDesc>) {
195    let mut walk = ignore::WalkBuilder::new(paths[0].clone());
196    for dir in paths.iter().skip(1) {
197        walk.add(dir);
198    }
199    walk.add_custom_ignore_filename(".cfgignore");
200
201    let config = Arc::new(config);
202    let lang_config = Arc::new(lang_config);
203
204    walk.build_parallel().run(move || {
205        let config = config.clone();
206        let lang_config = lang_config.clone();
207        Box::new(move |path| {
208            let path = path.unwrap();
209            // Skip dirs/symlinks
210            if !path.file_type().map(|f| f.is_file()).unwrap_or(false) {
211                return ignore::WalkState::Continue;
212            }
213            let extension = match path.path().extension() {
214                Some(v) => v,
215                None => return ignore::WalkState::Continue,
216            };
217            let extension = extension.to_string_lossy().to_string();
218            let desc = match lang_config.get(&extension) {
219                Some(v) => v,
220                None => return ignore::WalkState::Continue,
221            };
222            let desc = Rc::new(desc.clone());
223
224            let file = BufReader::new(File::open(path.path()).unwrap());
225            let mut out = BufWriter::new(
226                tempfile::NamedTempFile::new_in(path.path().parent().unwrap()).unwrap(),
227            );
228
229            for line in process(file.lines().map(|l| l.unwrap()), config.clone(), desc) {
230                writeln!(out, "{}", line).unwrap();
231            }
232
233            out.into_inner().unwrap().persist(path.path()).unwrap();
234
235            ignore::WalkState::Continue
236        })
237    });
238}