stripper_lib/
regenerate.rs

1// Copyright 2015 Gomez Guillaume
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use consts::{END_INFO, FILE, FILE_COMMENT, MOD_COMMENT};
16use std::collections::HashMap;
17use std::fs::{remove_file, File, OpenOptions};
18use std::io::{BufRead, BufReader, Write};
19use std::iter;
20use std::ops::Deref;
21use std::path::Path;
22use strip;
23use types::{EventType, ParseResult, Type, TypeStruct};
24use utils::{join, loop_over_files, remove_macro_parent, write_comment, write_file};
25
26type Infos = HashMap<Option<String>, Vec<(Option<TypeStruct>, Vec<String>)>>;
27
28fn gen_indent(indent: usize) -> String {
29    iter::repeat("    ")
30        .take(indent)
31        .collect::<Vec<&str>>()
32        .join("")
33}
34
35fn gen_indent_from(from: &str) -> String {
36    for (i, c) in from.chars().enumerate() {
37        if c != ' ' && c != '\t' {
38            return gen_indent(i / 4);
39        }
40    }
41    String::new()
42}
43
44/// Returns `true` in case a "// rustdoc-stripper-ignore-next-stop" was inserted.
45fn regenerate_comment(
46    is_file_comment: bool,
47    position: usize,
48    indent: usize,
49    comment: &str,
50    original_content: &mut Vec<String>,
51    need_check_ignore_doc_comment: bool,
52) -> bool {
53    let mut need_to_add_ignore_next_comment_stop = false;
54    if need_check_ignore_doc_comment && position > 0 {
55        let prev = original_content[position - 1].trim();
56        if strip::DOC_COMMENT_ID.iter().any(|d| prev.starts_with(d)) {
57            need_to_add_ignore_next_comment_stop = true;
58        }
59    }
60    let is_empty = comment.trim().is_empty();
61    let read_indent = if is_file_comment {
62        gen_indent(indent)
63    } else {
64        let tmp = original_content[position].clone();
65        gen_indent_from(&tmp)
66    };
67    original_content.insert(
68        position,
69        format!(
70            "{}{}{}{}",
71            &read_indent,
72            if is_file_comment { "//!" } else { "///" },
73            if is_empty { "" } else { " " },
74            if is_empty { "" } else { comment }
75        ),
76    );
77    if need_to_add_ignore_next_comment_stop {
78        original_content.insert(
79            position,
80            format!("{}{}", &read_indent, strip::IGNORE_NEXT_COMMENT_STOP,),
81        );
82    }
83    need_to_add_ignore_next_comment_stop
84}
85
86#[allow(clippy::useless_let_if_seq)]
87fn get_corresponding_type(
88    elements: &[(Option<TypeStruct>, Vec<String>)],
89    to_find: &Option<TypeStruct>,
90    mut line: usize,
91    decal: &mut usize,
92    original_content: &mut Vec<String>,
93    ignore_macros: bool,
94) -> Option<usize> {
95    if to_find.is_none() {
96        return None;
97    }
98    let to_find = to_find.as_ref().unwrap();
99    let mut pos = 0;
100
101    while pos < elements.len() {
102        if match elements[pos].0 {
103            Some(ref a) => {
104                // is true if a is a top level Type, or if is inside a macro and Type and name match
105                /* The result is that if there is a struct defined inside a macro,
106                the documentation (if it has) of that struct will be written inside the macro. */
107                let ret = a == to_find || {
108                    let mut tmp = to_find.clone();
109                    remove_macro_parent(&mut tmp);
110                    *a == tmp
111                };
112
113                // to detect variants
114                if !ret
115                    && to_find.ty == Type::Unknown
116                    && to_find.parent.is_some()
117                    && a.parent.is_some()
118                    && a.parent == to_find.parent
119                {
120                    if match to_find.parent {
121                        Some(ref p) => {
122                            p.ty == Type::Struct || p.ty == Type::Enum || p.ty == Type::Use
123                        }
124                        None => false,
125                    } {
126                        let mut tmp = to_find.clone();
127                        tmp.ty = Type::Variant;
128                        a == &tmp
129                    } else {
130                        false
131                    }
132                } else {
133                    ret
134                }
135            }
136            _ => false,
137        } {
138            let mut file_comment = false;
139
140            if !elements[pos].1.is_empty() && elements[pos].1[0].starts_with("//!") {
141                line += 1;
142                file_comment = true;
143            } else {
144                while line > 0
145                    && (line + *decal) > 0
146                    && original_content[line + *decal - 1]
147                        .trim_start()
148                        .starts_with('#')
149                {
150                    line -= 1;
151                }
152            }
153            let mut first = true;
154            for comment in elements[pos].1.iter().skip(usize::from(file_comment)) {
155                let depth = if let Some(ref e) = elements[pos].0 {
156                    e.get_depth(ignore_macros)
157                } else {
158                    0
159                };
160                if regenerate_comment(
161                    file_comment,
162                    line + *decal,
163                    depth + 1,
164                    comment,
165                    original_content,
166                    first,
167                ) {
168                    *decal += 1;
169                }
170                *decal += 1;
171                first = false;
172            }
173            return Some(pos);
174        }
175        pos += 1;
176    }
177    None
178}
179
180// The hashmap key is `Some(file name)` or `None` for entries that ignore file name
181pub fn regenerate_comments(
182    work_dir: &Path,
183    path: &str,
184    infos: &mut Infos,
185    ignore_macros: bool,
186    ignore_doc_commented: bool,
187) {
188    if !infos.contains_key(&None) && !infos.contains_key(&Some(path.to_owned())) {
189        return;
190    }
191    let full_path = work_dir.join(path);
192    match strip::build_event_list(&full_path) {
193        Ok(ref mut parse_result) => {
194            // exact path match
195            if let Some(v) = infos.get_mut(&Some(path.to_owned())) {
196                do_regenerate(
197                    &full_path,
198                    parse_result,
199                    v,
200                    ignore_macros,
201                    ignore_doc_commented,
202                );
203            }
204            // apply to all files
205            if let Some(v) = infos.get_mut(&None) {
206                do_regenerate(
207                    &full_path,
208                    parse_result,
209                    v,
210                    ignore_macros,
211                    ignore_doc_commented,
212                );
213            }
214        }
215        Err(e) => {
216            println!("Error in file '{}': {}", path, e);
217        }
218    }
219}
220
221fn check_if_regen(it: usize, parse_result: &ParseResult, ignore_doc_commented: bool) -> bool {
222    ignore_doc_commented
223        && it > 0
224        && matches!(
225            parse_result.event_list[it - 1].event,
226            EventType::Comment(_) | EventType::FileComment(_)
227        )
228}
229
230fn do_regenerate(
231    path: &Path,
232    parse_result: &mut ParseResult,
233    elements: &mut Vec<(Option<TypeStruct>, Vec<String>)>,
234    ignore_macros: bool,
235    ignore_doc_commented: bool,
236) {
237    let mut position = 0;
238    let mut decal = 0;
239
240    // first, we need to put back file comment
241    for entry in elements.iter() {
242        if entry.0.is_none() {
243            let mut it = 0;
244
245            while it < parse_result.original_content.len()
246                && parse_result.original_content[it].starts_with('/')
247            {
248                it += 1;
249            }
250            if it > 0 {
251                it += 1;
252            }
253            if it < parse_result.original_content.len() {
254                for line in &entry.1 {
255                    if line.trim().is_empty() {
256                        parse_result.original_content.insert(it, "//!".to_string());
257                    } else {
258                        parse_result
259                            .original_content
260                            .insert(it, format!("//! {}", &line));
261                    }
262                    decal += 1;
263                    it += 1;
264                }
265            }
266            parse_result.original_content.insert(it, "".to_owned());
267            decal += 1;
268            break;
269        }
270        position += 1;
271    }
272    if position < elements.len() {
273        elements.remove(position);
274    }
275    let mut waiting_type = None;
276    let mut current = None;
277    let mut it = 0;
278
279    while it < parse_result.event_list.len() {
280        match parse_result.event_list[it].event {
281            EventType::Type(ref t) => {
282                if t.ty != Type::Unknown {
283                    waiting_type = Some(t.clone());
284                    let tmp = {
285                        let t = strip::add_to_type_scope(&current, &waiting_type);
286                        if ignore_macros {
287                            erase_macro_path(t)
288                        } else {
289                            t
290                        }
291                    };
292
293                    if !check_if_regen(it, parse_result, ignore_doc_commented) {
294                        if let Some(l) = get_corresponding_type(
295                            elements,
296                            &tmp,
297                            parse_result.event_list[it].line,
298                            &mut decal,
299                            &mut parse_result.original_content,
300                            ignore_macros,
301                        ) {
302                            elements.remove(l);
303                        }
304                    }
305                } else if let Some(ref c) = current {
306                    if c.ty == Type::Struct || c.ty == Type::Enum || c.ty == Type::Mod {
307                        let tmp = Some(t.clone());
308                        let cc = {
309                            let t = strip::add_to_type_scope(&current, &tmp);
310                            if ignore_macros {
311                                erase_macro_path(t)
312                            } else {
313                                t
314                            }
315                        };
316
317                        if !check_if_regen(it, parse_result, ignore_doc_commented) {
318                            if let Some(l) = get_corresponding_type(
319                                elements,
320                                &cc,
321                                parse_result.event_list[it].line,
322                                &mut decal,
323                                &mut parse_result.original_content,
324                                ignore_macros,
325                            ) {
326                                elements.remove(l);
327                            }
328                        }
329                    }
330                }
331            }
332            EventType::InScope => {
333                current = strip::add_to_type_scope(&current, &waiting_type);
334                waiting_type = None;
335            }
336            EventType::OutScope => {
337                current = strip::type_out_scope(&current);
338                waiting_type = None;
339            }
340            _ => {}
341        }
342        it += 1;
343    }
344    rewrite_file(path, &parse_result.original_content);
345}
346
347fn rewrite_file(path: &Path, o_content: &[String]) {
348    match File::create(path) {
349        Ok(mut f) => {
350            write!(f, "{}", o_content.join("\n")).unwrap();
351        }
352        Err(e) => {
353            println!("Cannot open '{}': {}", path.display(), e);
354        }
355    }
356}
357
358fn parse_mod_line(line: &str) -> Option<TypeStruct> {
359    let line = line
360        .replace(FILE_COMMENT, "")
361        .replace(MOD_COMMENT, "")
362        .replace(END_INFO, "");
363    if line.is_empty() {
364        return None;
365    }
366    let parts: Vec<&str> = line.split("::").collect();
367    let mut current = None;
368
369    for part in parts {
370        let elems: Vec<&str> = part.split(' ').filter(|x| !x.is_empty()).collect();
371
372        current = strip::add_to_type_scope(
373            &current.clone(),
374            &Some(TypeStruct::new(
375                Type::from(elems[0]),
376                elems[elems.len() - 1],
377            )),
378        );
379    }
380    current
381}
382
383fn save_remainings(infos: &Infos, comment_file: &str) {
384    let mut remainings = 0;
385
386    for content in infos.values() {
387        if !content.is_empty() {
388            remainings += 1;
389        }
390    }
391    if remainings < 1 {
392        let _ = remove_file(comment_file);
393        return;
394    }
395    match File::create(comment_file) {
396        Ok(mut out_file) => {
397            println!(
398                "Some comments haven't been regenerated to the files. Saving them \
399                      back to '{}'.",
400                comment_file
401            );
402            for (key, content) in infos {
403                if content.is_empty() {
404                    continue;
405                }
406                // Set the name to "*" for entries that ignore file name
407                let key = key.as_ref().map(|s| &s[..]).unwrap_or("*");
408                let _ = writeln!(out_file, "{}", &write_file(key));
409                for line in content {
410                    if let Some(ref d) = line.0 {
411                        let _ = writeln!(
412                            out_file,
413                            "{}",
414                            write_comment(d, &join(&line.1, "\n"), false)
415                        );
416                    }
417                }
418            }
419        }
420        Err(e) => {
421            println!(
422                "An error occured while trying to open '{}': {}",
423                comment_file, e
424            );
425        }
426    }
427}
428
429pub fn regenerate_doc_comments(
430    directory: &str,
431    verbose: bool,
432    comment_file: &str,
433    ignore_macros: bool,
434    ignore_doc_commented: bool,
435) {
436    // we start by storing files info
437    let f = match OpenOptions::new().read(true).open(comment_file) {
438        Ok(f) => f,
439        Err(e) => {
440            println!(
441                "An error occured while trying to open '{}': {}",
442                comment_file, e
443            );
444            return;
445        }
446    };
447    let reader = BufReader::new(f);
448    let lines = reader.lines().map(|line| line.unwrap());
449    let mut infos = parse_cmts(lines, ignore_macros);
450    let ignores: &[&str] = &[];
451
452    loop_over_files(
453        directory.as_ref(),
454        &mut |w, s| regenerate_comments(w, s, &mut infos, ignore_macros, ignore_doc_commented),
455        ignores,
456        verbose,
457    );
458    save_remainings(&infos, comment_file);
459}
460
461fn sub_erase_macro_path(ty: Option<Box<TypeStruct>>, is_parent: bool) -> Option<Box<TypeStruct>> {
462    match ty {
463        Some(ref t) if is_parent => {
464            if t.ty == Type::Macro {
465                sub_erase_macro_path(t.clone().parent, true)
466            } else {
467                let mut tmp = t.clone();
468                tmp.parent = sub_erase_macro_path(t.clone().parent, true);
469                Some(tmp)
470            }
471        }
472        Some(t) => {
473            let mut tmp = t.clone();
474            tmp.parent = sub_erase_macro_path(t.parent, true);
475            Some(tmp)
476        }
477        None => None,
478    }
479}
480
481fn erase_macro_path(ty: Option<TypeStruct>) -> Option<TypeStruct> {
482    ty.map(|t| *sub_erase_macro_path(Some(Box::new(t)), false).unwrap())
483}
484
485pub fn parse_cmts<S, I>(lines: I, ignore_macros: bool) -> Infos
486where
487    S: Deref<Target = str>,
488    I: Iterator<Item = S>,
489{
490    enum State {
491        Initial,
492        File {
493            file: Option<String>,
494            infos: Vec<(Option<TypeStruct>, Vec<String>)>,
495            ty: Option<TypeStruct>,
496            comments: Vec<String>,
497        },
498    }
499
500    // Returns `Some(name)` if the line matches FILE
501    // where name is Some for an actual file name and None for "*"
502    // The "*" entries are to be applied regardless of file name
503    #[allow(clippy::option_option)]
504    fn line_file(line: &str) -> Option<Option<String>> {
505        if let Some(after) = line.strip_prefix(FILE) {
506            let name = after.replace(END_INFO, "");
507            if name == "*" {
508                Some(None)
509            } else {
510                Some(Some(name))
511            }
512        } else {
513            None
514        }
515    }
516
517    let mut ret = HashMap::new();
518    let mut state = State::Initial;
519
520    for line in lines {
521        state = match state {
522            State::Initial => {
523                if let Some(file) = line_file(&line) {
524                    State::File {
525                        file,
526                        infos: vec![],
527                        ty: None,
528                        comments: vec![],
529                    }
530                } else {
531                    panic!("Unrecognized format on line: `{}`", line.deref());
532                }
533            }
534            State::File {
535                mut file,
536                mut infos,
537                mut ty,
538                mut comments,
539            } => {
540                if let Some(new_file) = line_file(&line) {
541                    if !comments.is_empty() {
542                        infos.push((ty.take(), comments));
543                        comments = vec![];
544                    }
545                    if !infos.is_empty() {
546                        ret.insert(file, infos);
547                        file = new_file;
548                        infos = vec![];
549                    }
550                } else if line.starts_with(FILE_COMMENT) {
551                    if let Some(ty) = ty.take() {
552                        if !comments.is_empty() {
553                            infos.push((Some(ty), comments));
554                            comments = vec!["//!".to_owned()];
555                        }
556                    } else if !comments.is_empty() {
557                        infos.push((None, comments));
558                        comments = vec![];
559                    }
560                    ty = parse_mod_line(&line[..]);
561                } else if line.starts_with(MOD_COMMENT) {
562                    if !comments.is_empty() {
563                        infos.push((ty, comments));
564                        comments = vec![];
565                    }
566                    ty = parse_mod_line(&line[..]);
567                } else {
568                    comments.push(line[..].to_owned());
569                }
570                State::File {
571                    file,
572                    infos,
573                    ty: if ignore_macros {
574                        erase_macro_path(ty)
575                    } else {
576                        ty
577                    },
578                    comments,
579                }
580            }
581        }
582    }
583
584    if let State::File {
585        file,
586        mut infos,
587        ty,
588        comments,
589    } = state
590    {
591        if !comments.is_empty() {
592            infos.push((ty, comments));
593        }
594        if !infos.is_empty() {
595            ret.insert(file, infos);
596        }
597    }
598
599    ret
600}