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 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}