use std::fs::File;
use std::io::{self, Read, Write};
use std::ops::Deref;
use std::path::Path;
use std::process::exit;
use types::{EventInfo, EventType, ParseResult, Type, TypeStruct};
use utils::{join, write_comment, write_file, write_file_comment};
const STOP_CHARACTERS: &[char] = &['\t', '\n', '\r', '<', '{', ':', ';', '!', '(', ','];
const COMMENT_ID: &[&str] = &["//", "/*"];
pub(crate) const DOC_COMMENT_ID: &[&str] = &["///", "/*!", "//!", "/**"];
pub(crate) const IGNORE_NEXT_COMMENT: &str = "// rustdoc-stripper-ignore-next";
pub(crate) const IGNORE_NEXT_COMMENT_STOP: &str = "// rustdoc-stripper-ignore-next-stop";
fn move_to(words: &[&str], it: &mut usize, limit: &str, line: &mut usize, start_remove: &str) {
if words[*it][start_remove.len()..].contains(limit) {
return;
}
*it += 1;
while let Some(&word) = words.get(*it) {
if words[*it].contains(limit) {
break;
}
if word == "\n" {
*line += 1;
}
*it += 1;
}
if let Some(&"\n") = words.get(*it) {
*line += 1;
}
}
fn move_until(words: &[&str], it: &mut usize, limit: &str, line: &mut usize) {
let alternative1 = format!("{};", limit);
let alternative2 = format!("{}\n", limit);
while *it < words.len()
&& !words[*it].ends_with(limit)
&& !words[*it].ends_with(&alternative1)
&& !words[*it].ends_with(&alternative2)
{
*line += words[*it].chars().filter(|c| *c == '\n').count();
*it += 1;
}
}
fn get_before<'a>(word: &'a str, limits: &[char]) -> &'a str {
word.find(limits).map(|pos| &word[..pos]).unwrap_or(word)
}
fn get_impl(words: &[&str], it: &mut usize, line: &mut usize) -> Vec<String> {
let mut v = vec![];
while *it + 1 < words.len() {
if words[*it] == "\n" {
*line += 1;
}
if words[*it + 1] == "{" || words[*it + 1] == ";" {
break;
}
*it += 1;
v.push(words[*it].to_owned());
}
v
}
pub fn add_to_type_scope(
current: &Option<TypeStruct>,
e: &Option<TypeStruct>,
) -> Option<TypeStruct> {
match *current {
Some(ref c) => match *e {
Some(ref t) => {
let mut tmp = t.clone();
tmp.parent = Some(Box::new(c.clone()));
Some(tmp)
}
_ => {
let mut tmp = TypeStruct::empty();
tmp.parent = Some(Box::new(c.clone()));
Some(tmp)
}
},
None => e.as_ref().cloned(),
}
}
pub fn type_out_scope(current: &Option<TypeStruct>) -> Option<TypeStruct> {
current
.as_ref()
.and_then(|c| c.parent.as_ref().map(|p| p.deref().clone()))
}
fn get_mod(current: &Option<TypeStruct>) -> bool {
match *current {
Some(ref t) => {
if t.ty != Type::Mod {
println!("Mod/File comments cannot be put here!");
false
} else {
true
}
}
None => true,
}
}
enum BlockKind<'a> {
Comment((String, String, &'a str)),
DocComment((String, String, &'a str)),
Other(&'a str),
}
fn get_three_parts<'a>(
before: &'a str,
comment_sign: &str,
after: &'a str,
stop: &str,
) -> (String, String, &'a str) {
if let Some(pos) = after.find(stop) {
let extra = if stop != "\n" { stop.len() } else { 0 };
(
before.to_owned(),
format!("{} {}", comment_sign, &after[..pos]),
&after[pos + extra..],
)
} else {
(
before.to_owned(),
format!("{} {}", comment_sign, &after),
&after[after.len() - 1..],
)
}
}
fn check_if_should_be_ignored(text: &str) -> bool {
let mut ignore_until_multi_end = false;
for line in text.split('\n').rev() {
let line = line.trim();
if !ignore_until_multi_end && !line.starts_with("//") {
if line.trim().ends_with("*/") {
ignore_until_multi_end = !line.starts_with("/**") && !line.starts_with("/*!");
continue;
}
} else if line.starts_with("/*") {
ignore_until_multi_end = false;
continue;
}
if !ignore_until_multi_end {
if line == IGNORE_NEXT_COMMENT_STOP {
return false;
} else if line == IGNORE_NEXT_COMMENT {
return true;
}
if !line.starts_with("///") && !line.starts_with("//!") {
break;
}
}
}
false
}
fn find_one_of<'a>(comments: &[&str], doc_comments: &[&str], text: &'a str) -> BlockKind<'a> {
let mut last_pos = 0;
let mut tmp_text = &text[last_pos..];
while let Some(pos) = tmp_text.find('/') {
tmp_text = &tmp_text[pos..];
last_pos += pos;
for com in doc_comments {
if let Some(after) = tmp_text.strip_prefix(com) {
if &com[1..2] == "*" {
return BlockKind::DocComment(get_three_parts(
&text[..last_pos],
com,
after,
"*/",
));
} else {
return BlockKind::DocComment(get_three_parts(
&text[..last_pos],
com,
after,
"\n",
));
}
}
}
for com in comments {
if tmp_text.starts_with(com) {
if &com[1..2] == "*" {
return BlockKind::Comment(get_three_parts(
&text[0..last_pos],
"",
tmp_text,
"*/",
));
} else {
return BlockKind::Comment(get_three_parts(
&text[0..last_pos],
"",
tmp_text,
"\n",
));
}
}
}
if !tmp_text.is_empty() {
tmp_text = &tmp_text[1..];
last_pos += 1;
} else {
break;
}
}
BlockKind::Other(text)
}
fn transform_code(code: &str) -> String {
code.replace('{', " { ")
.replace('}', " } ")
.replace(':', " : ")
.replace(" : : ", "::")
.replace("*/", " */")
.replace('\n', " \n ")
.replace("!(", " !! (")
.replace("! {", " !? {")
.replace(',', ", ")
.replace('(', " (")
.replace('"', " \"")
}
fn clean_input(s: &str) -> String {
let mut ret = String::new();
let mut text = s;
loop {
text = match find_one_of(COMMENT_ID, DOC_COMMENT_ID, text) {
BlockKind::Other(content) => {
ret.push_str(&transform_code(content));
break;
}
BlockKind::DocComment((before, doc_comment, after))
if !check_if_should_be_ignored(&s[..s.len() - after.len()]) =>
{
ret.push_str(&transform_code(&before));
ret.push_str(&doc_comment);
after
}
BlockKind::DocComment((before, doc_comment, after)) => {
ret.push_str(&transform_code(&before));
for _ in 0..doc_comment.split('\n').count() - 1 {
ret.push_str(" \n ");
}
after
}
BlockKind::Comment((before, comment, after)) => {
ret.push_str(&transform_code(&before));
for _ in 0..comment.split('\n').count() - 1 {
ret.push_str(" \n ");
}
after
}
};
}
ret
}
fn clear_events(mut events: Vec<EventInfo>) -> Vec<EventInfo> {
let mut current: Option<TypeStruct> = None;
let mut waiting_type: Option<TypeStruct> = None;
let mut it = 0;
while it < events.len() {
if match events[it].event {
EventType::Type(ref t) => {
if t.ty != Type::Unknown {
waiting_type = Some(t.clone());
false
} else if let Some(ref parent) = current {
!matches!(parent.ty, Type::Struct | Type::Enum)
} else {
true
}
}
EventType::InScope => {
current = add_to_type_scope(¤t, &waiting_type);
waiting_type = None;
false
}
EventType::OutScope => {
current = type_out_scope(¤t);
waiting_type = None;
false
}
_ => false,
} {
events.remove(it);
continue;
}
it += 1;
}
events
}
fn remove_stop_chars(s: &str) -> String {
let mut s = s.to_owned();
for c in STOP_CHARACTERS {
if s.contains(*c) {
s = s.replace(&c.to_string(), "");
}
}
s
}
#[allow(clippy::useless_let_if_seq)]
fn build_event_inner(
it: &mut usize,
line: &mut usize,
words: &[&str],
event_list: &mut Vec<EventInfo>,
comment_lines: &mut Vec<usize>,
b_content: &[String],
mut par_count: Option<isize>,
) {
let mut waiting_for_macro = false;
while *it < words.len() {
match words[*it] {
c if c.starts_with('"') => move_to(words, it, "\"", line, "\""),
c if c.starts_with("b\"") => move_to(words, it, "\"", line, "b\""),
c if c.starts_with("r#") => {
let end = c.split("#\"").next().unwrap().replace(['"', 'r'], "");
move_to(words, it, &format!("\"{}", end), line, "r#");
}
"///" | "///\n" => {
comment_lines.push(*line);
event_list.push(EventInfo::new(
*line,
EventType::Comment(b_content[*line].to_owned()),
));
move_to(words, it, "\n", line, "");
}
"//!" | "//!\n" => {
comment_lines.push(*line);
event_list.push(EventInfo::new(
*line,
EventType::FileComment(b_content[*line].to_owned()),
));
if *line + 1 < b_content.len() && b_content[*line + 1].is_empty() {
comment_lines.push(*line + 1);
}
move_to(words, it, "\n", line, "");
}
"/*!" | "/*!\n" => {
let mark = *line;
move_until(words, it, "*/", line);
for (pos, s) in b_content.iter().enumerate().take(*line).skip(mark) {
comment_lines.push(pos);
event_list.push(EventInfo::new(*line, EventType::FileComment(s.to_owned())));
}
comment_lines.push(*line);
let mut removed = false;
if *line + 1 < b_content.len() && b_content[*line + 1].is_empty() {
comment_lines.push(*line + 1);
removed = true;
}
event_list.push(EventInfo::new(
mark,
EventType::FileComment("*/".to_owned()),
));
if removed {
event_list.push(EventInfo::new(*line, EventType::FileComment("".to_owned())));
}
}
"/**" | "/**\n" => {
let mark = *line;
move_until(words, it, "*/", line);
for (pos, s) in b_content.iter().enumerate().take(*line).skip(mark) {
comment_lines.push(pos);
event_list.push(EventInfo::new(*line, EventType::Comment(s.to_owned())));
}
comment_lines.push(*line);
let mut removed = false;
if *line + 1 < b_content.len() && b_content[*line + 1].is_empty() {
comment_lines.push(*line + 1);
removed = true;
}
event_list.push(EventInfo::new(
mark,
EventType::FileComment("*/".to_owned()),
));
if removed {
event_list.push(EventInfo::new(*line, EventType::Comment("".to_owned())));
}
}
"use" | "mod" => {
let mut name = words[*it + 1].to_owned();
let ty = words[*it];
if *line + 1 < b_content.len() && b_content[*line].ends_with("::{") {
move_to(words, it, "\n", line, "");
name.push_str(b_content[*line + 1].trim());
}
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(Type::from(ty), &name)),
));
}
"struct" | "enum" | "const" | "static" | "type" | "trait" | "macro_rules!"
| "flags" => {
if *it + 1 >= words.len() {
break;
}
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(
Type::from(words[*it]),
get_before(words[*it + 1], STOP_CHARACTERS),
)),
));
waiting_for_macro = words[*it] == "macro_rules!";
*it += 1;
}
"fn" => {
if *it + 1 >= words.len() {
break;
}
let name = get_before(words[*it + 1], STOP_CHARACTERS);
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(Type::from(words[*it]), name)),
));
*it += 1;
if !name.is_empty() {
while let Some(&word) = words.get(*it) {
if word.ends_with(';') {
break;
}
if word.starts_with('{') {
*it -= 1;
break;
}
if word == "\n" {
*line += 1;
}
*it += 1;
}
}
}
"!!" => {
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(
Type::from("macro"),
&format!("{}!{}", words[*it - 1], words[*it + 1]),
)),
));
*it += 1;
}
"!?" => {
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(
Type::from("macro"),
&format!("{}!", words[*it - 1]),
)),
));
}
"impl" => {
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(
Type::Impl,
&join(&get_impl(words, it, line), " "),
)),
));
}
c if c.starts_with("impl<") => {
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(
Type::Impl,
&join(&get_impl(words, it, line), " "),
)),
));
}
x if x == "{" || x == "{\n" => {
if let Some(ref mut par_count) = par_count {
*par_count += 1;
}
event_list.push(EventInfo::new(*line, EventType::InScope));
if waiting_for_macro {
build_event_inner(
it,
line,
words,
&mut vec![],
&mut vec![],
b_content,
Some(1),
);
waiting_for_macro = false;
}
if x == "}\n" {
*line += 1;
}
}
x if x == "}" || x == "}\n" => {
if let Some(ref mut par_count) = par_count {
*par_count -= 1;
if *par_count <= 0 {
return;
}
}
event_list.push(EventInfo::new(*line, EventType::OutScope));
if x == "}\n" {
*line += 1;
}
}
"\n" => {
*line += 1;
}
s if s.starts_with("#[") || s.starts_with("#![") => {
while *it < words.len() {
*line += words[*it].split('\n').count() - 1;
if words[*it].contains(']') {
break;
}
*it += 1;
}
*line += s.chars().filter(|c| *c == '\n').count();
}
x => {
event_list.push(EventInfo::new(
*line,
EventType::Type(TypeStruct::new(
Type::Unknown,
&remove_stop_chars(words[*it]),
)),
));
*line += x.chars().filter(|c| *c == '\n').count();
}
}
*it += 1;
}
}
pub fn build_event_list(path: &Path) -> io::Result<ParseResult> {
let mut f = File::open(path)?;
let mut b_content = String::new();
f.read_to_string(&mut b_content).unwrap();
let content = clean_input(&b_content);
let b_content: Vec<String> = b_content.split('\n').map(|s| s.to_owned()).collect();
let words: Vec<&str> = content.split(' ').filter(|s| !s.is_empty()).collect();
let mut it = 0;
let mut line = 0;
let mut event_list = vec![];
let mut comment_lines = vec![];
build_event_inner(
&mut it,
&mut line,
&words,
&mut event_list,
&mut comment_lines,
&b_content,
None,
);
let clear = clear_events(event_list);
Ok(ParseResult {
event_list: clear,
comment_lines,
original_content: b_content,
})
}
fn unformat_comment(c: &str) -> String {
fn remove_prepend(s: &str) -> String {
let mut s = s.to_owned();
for to_remove in DOC_COMMENT_ID {
s = s.replace(to_remove, "");
}
if s.starts_with(' ') {
(&s)[1..].to_owned()
} else {
s
}
}
c.replace("*/", "")
.split('\n')
.map(|s| remove_prepend(s.trim_start()))
.collect::<Vec<String>>()
.join("\n")
}
pub fn strip_comments<F: Write>(
work_dir: &Path,
path: &str,
out_file: &mut F,
ignore_macros: bool,
) {
let full_path = work_dir.join(path);
match build_event_list(&full_path) {
Ok(parse_result) => {
if parse_result.comment_lines.is_empty() {
return;
}
writeln!(out_file, "{}", &write_file(path)).unwrap();
let mut current: Option<TypeStruct> = None;
let mut waiting_type: Option<TypeStruct> = None;
let mut it = 0;
while it < parse_result.event_list.len() {
match parse_result.event_list[it].event {
EventType::Type(ref t) => {
if t.ty != Type::Unknown {
waiting_type = Some(t.clone());
}
}
EventType::InScope => {
current = add_to_type_scope(¤t, &waiting_type);
waiting_type = None;
}
EventType::OutScope => {
current = type_out_scope(¤t);
waiting_type = None;
}
EventType::FileComment(ref c) => {
if !get_mod(¤t) {
exit(1);
}
it += 1;
let mut comments = format!(
"{}\n",
&write_file_comment(&unformat_comment(c), ¤t, ignore_macros)
);
while parse_result
.event_list
.get(it)
.map(|x| match x.event {
EventType::FileComment(ref c) => {
use std::fmt::Write;
writeln!(comments, "{}", unformat_comment(c)).unwrap();
true
}
_ => false,
})
.unwrap_or(false)
{
it += 1;
}
write!(out_file, "{}", comments).unwrap();
continue;
}
EventType::Comment(ref c) => {
let mut comments = format!("{}\n", c);
it += 1;
while it < parse_result.event_list.len()
&& match parse_result.event_list[it].event {
EventType::Comment(ref c) => {
use std::fmt::Write;
writeln!(comments, "{}", c).unwrap();
true
}
EventType::Type(_) => false,
_ => panic!("[{}:{}]: Doc comments cannot be written everywhere:\n---> {:#?}", full_path.display(), parse_result.event_list[it].line, parse_result.event_list),
}
{
it += 1;
}
while parse_result
.event_list
.get(it)
.map(|x| match x.event {
EventType::Type(ref t) => match t.ty {
Type::Unknown => match current {
Some(ref cur) => {
if cur.ty == Type::Enum
|| cur.ty == Type::Struct
|| cur.ty == Type::Use
{
if t.name == "pub" {
true
} else {
let mut copy = t.clone();
copy.ty = Type::Variant;
let tmp =
add_to_type_scope(¤t, &Some(copy));
write!(
out_file,
"{}",
write_comment(
&tmp.unwrap(),
&unformat_comment(&comments),
ignore_macros
)
)
.unwrap();
false
}
} else {
t.name == "pub"
}
}
None => t.name == "pub",
},
_ => {
let tmp = add_to_type_scope(¤t, &Some(t.clone()));
write!(
out_file,
"{}",
write_comment(
&tmp.unwrap(),
&unformat_comment(&comments),
ignore_macros
)
)
.unwrap();
false
}
},
_ => panic!("An item was expected for this comment: {}", comments),
})
.unwrap_or(false)
{
it += 1;
}
continue;
}
}
it += 1;
}
remove_comments(
&full_path,
&parse_result.comment_lines,
parse_result.original_content,
);
}
Err(e) => {
println!("Unable to open \"{}\": {}", path, e);
}
}
}
fn remove_comments(path: &Path, to_remove: &[usize], mut o_content: Vec<String>) {
let mut decal = 0;
match File::create(path) {
Ok(mut f) => {
for line in to_remove.iter() {
if line - decal > 0
&& line - decal + 1 < o_content.len()
&& o_content[line - decal - 1].trim() == IGNORE_NEXT_COMMENT_STOP
{
let l = o_content[line - decal + 1].trim();
if DOC_COMMENT_ID.iter().any(|d| l.starts_with(d)) {
o_content.remove(line - decal - 1);
decal += 1;
}
}
o_content.remove(line - decal);
decal += 1;
}
write!(f, "{}", o_content.join("\n")).unwrap();
}
Err(e) => {
println!("Cannot open '{}': {}", path.display(), e);
}
}
}