zamm_yang 0.1.8

A basic, experimental code generator
Documentation
/// The comment string that will be placed before every line of autogenerated code.
pub const AUTOGENERATION_MARKER: &str = "// AUTOGENERATED CODE -- DO NOT EDIT";

/// The number of spaces that a tab is considered equivalent to.
const SPACES_IN_TAB: usize = 4;

/// Returns the indent, as well as the first non-whitespace character.
pub fn count_indent(line: &str) -> (usize, Option<char>) {
    let mut count = 0;
    let mut non_whitespace: Option<char> = None;
    for c in line.chars() {
        match c {
            ' ' => count += 1,
            '\t' => count += SPACES_IN_TAB,
            other => {
                non_whitespace = Some(other);
                break;
            }
        }
    }
    (count, non_whitespace)
}

/// Indent a line by a certain number of spaces.
pub fn add_indent(indent_size: usize, line: &str) -> String {
    if line.is_empty() {
        line.to_owned() // don't generate indented empty lines
    } else {
        " ".repeat(indent_size) + line
    }
}

/// Adds an autogeneration comment to this line of code, if applicable. Returns the commented line
/// as well as whether or not the line was a comment.
fn add_comment(line: &str, comment: &str, previous_was_comment: bool) -> (String, bool) {
    let (indent_size, first_char) = count_indent(line);
    let is_comment = first_char == Some('/');
    if previous_was_comment {
        return (line.to_string(), is_comment);
    }
    let code_line = match first_char {
        Some('}') | Some(')') | Some(']') => {
            // avoid commenting at all, because it turns out `cargo fmt` will auto-align comments
            // in cases such as this:
            //
            // ); // AUTOGENERATED CODE -- DO NOT EDIT
            // // AUTOGENERATED CODE -- DO NOT EDIT
            // assert_eq!(
            //
            // and turn it into this:
            //
            // ); // AUTOGENERATED CODE -- DO NOT EDIT
            //    // AUTOGENERATED CODE -- DO NOT EDIT
            // assert_eq!(
            line.to_string()
        }
        Some('/') => line.to_string(), // no need to comment on a comment
        Some('#') => {
            // no need to comment on attributes, especially ones at the beginning of a file
            line.to_string()
        }
        Some(_) => {
            let comment_line = add_indent(indent_size, comment);
            format!("{}\n{}", comment_line, line)
        }
        None => line.to_string(), // no need to comment on whitespace
    };
    (code_line, is_comment)
}

/// Add autogeneration comments to all applicable lines of code. Skips empty lines and comments.
pub fn add_autogeneration_comments(code: &str) -> String {
    if code.is_empty() {
        return code.to_string(); // edge case to avoid inserting a superfluous newline
    }

    let mut result = String::new();
    let mut was_comment = false;
    for line in code.split('\n') {
        let (commented_line, is_comment) = add_comment(line, AUTOGENERATION_MARKER, was_comment);
        was_comment = is_comment;
        result.push_str(commented_line.as_str());
        result.push('\n');
    }
    if result.ends_with("\n\n") {
        // happens if input code already contains trailing newline
        result.pop();
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use indoc::indoc;

    #[test]
    fn count_empty_str() {
        assert_eq!(count_indent(""), (0, None));
    }

    #[test]
    fn count_no_indent() {
        assert_eq!(count_indent("fn main() {"), (0, Some('f')));
    }

    #[test]
    fn count_space_indent() {
        assert_eq!(count_indent("   fn main() {"), (3, Some('f')));
    }

    #[test]
    fn count_tab_indent() {
        assert_eq!(count_indent("\tfn main() {"), (4, Some('f')));
    }

    #[test]
    fn count_whitespace_only() {
        assert_eq!(count_indent("\t   \t"), (11, None));
    }

    #[test]
    fn count_mixed_indent() {
        // but why would you do that?!
        assert_eq!(count_indent("   \t  fn main() {"), (9, Some('f')));
    }

    #[test]
    fn test_add_no_indent() {
        assert_eq!(add_indent(0, "hello"), "hello".to_owned());
    }

    #[test]
    fn test_add_indent() {
        assert_eq!(add_indent(4, "hello"), "    hello".to_owned());
    }

    #[test]
    fn test_add_indent_empty() {
        assert_eq!(add_indent(4, ""), "".to_owned());
    }

    #[test]
    fn test_comment_empty_str() {
        assert_eq!(add_autogeneration_comments(""), "");
    }

    #[test]
    fn test_comment_newline_str() {
        assert_eq!(add_autogeneration_comments("\n"), "\n");
    }

    #[test]
    fn test_comment_statement() {
        assert_eq!(
            add_autogeneration_comments(indoc! {"
            return code.to_string();
            "}),
            indoc! {"
            // AUTOGENERATED CODE -- DO NOT EDIT
            return code.to_string();
            "}
        );
    }

    #[test]
    fn test_comment_indent_and_bracket() {
        assert_eq!(
            add_autogeneration_comments(indoc! {"
            if code.is_empty() {
                return code.to_string(); // no point in adding an extra newline to an empty string
            }
            "}),
            indoc! {"
            // AUTOGENERATED CODE -- DO NOT EDIT
            if code.is_empty() {
                // AUTOGENERATED CODE -- DO NOT EDIT
                return code.to_string(); // no point in adding an extra newline to an empty string
            }
            "}
        );
    }

    #[test]
    fn test_comment_closing_parentheses() {
        assert_eq!(
            add_autogeneration_comments(indoc! {"
            assert_eq!(
                Target::archetype().internal_name(),
                Some(Rc::new(Target::TYPE_NAME.to_string()))
            );
            "}),
            indoc! {"
            // AUTOGENERATED CODE -- DO NOT EDIT
            assert_eq!(
                // AUTOGENERATED CODE -- DO NOT EDIT
                Target::archetype().internal_name(),
                // AUTOGENERATED CODE -- DO NOT EDIT
                Some(Rc::new(Target::TYPE_NAME.to_string()))
            );
            "}
        );
    }

    #[test]
    fn test_comment_comment() {
        assert_eq!(
            add_autogeneration_comments(indoc! {"
            if code.is_empty() {
                // no point in adding an extra newline to an empty string
                return code.to_string();
            }
            "}),
            indoc! {"
            // AUTOGENERATED CODE -- DO NOT EDIT
            if code.is_empty() {
                // no point in adding an extra newline to an empty string
                return code.to_string();
            }
            "}
        );
    }

    #[test]
    fn test_comment_preamble() {
        assert_eq!(
            add_autogeneration_comments(indoc! {"
                #![allow(dead_code)]

                use something::unused;
            "}),
            indoc! {"
                #![allow(dead_code)]

                // AUTOGENERATED CODE -- DO NOT EDIT
                use something::unused;
            "}
        );
    }

    #[test]
    fn test_comment_whitespace() {
        assert_eq!(
            add_autogeneration_comments(indoc! {"
            let b = c;

            let a = b;
            "}),
            indoc! {"
            // AUTOGENERATED CODE -- DO NOT EDIT
            let b = c;

            // AUTOGENERATED CODE -- DO NOT EDIT
            let a = b;
            "}
        );
    }
}