#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Item {
pub text: String,
pub key: String,
pub code_at: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pieces {
pub items: Vec<Item>,
pub tail: String,
}
impl Pieces {
pub fn keys(&self) -> Vec<&str> {
self.items.iter().map(|i| i.key.as_str()).collect()
}
}
pub fn pieces(text: &str) -> Pieces {
let mut scanner = Scanner::default();
let mut items: Vec<Item> = Vec::new();
let mut pending = String::new();
let mut logical = String::new();
let mut key = String::new();
for line in text.split_inclusive('\n') {
let code = scanner.line(line);
let continues = continued(line);
if logical.is_empty() && code.is_empty() && !continues {
pending.push_str(line);
continue;
}
logical.push_str(line);
if !code.is_empty() {
if !key.is_empty() {
key.push(' ');
}
key.push_str(&code);
}
if continues || scanner.in_comment {
continue;
}
let mut piece = std::mem::take(&mut pending);
let code_at = piece.len();
piece.push_str(&logical);
logical.clear();
items.push(Item { text: piece, key: std::mem::take(&mut key), code_at });
}
if !logical.is_empty() {
let mut piece = std::mem::take(&mut pending);
let code_at = piece.len();
piece.push_str(&logical);
items.push(Item { text: piece, key: std::mem::take(&mut key), code_at });
}
Pieces { items, tail: pending }
}
pub fn code(text: &str) -> String {
let mut out = String::new();
for item in &pieces(text).items {
if item.key.is_empty() {
continue;
}
if !out.is_empty() {
out.push('\n');
}
out.push_str(&item.key);
}
out
}
fn continued(line: &str) -> bool {
line.trim_end_matches(['\n', '\r']).ends_with('\\')
}
#[derive(Default)]
struct Scanner {
in_comment: bool,
}
impl Scanner {
fn line(&mut self, text: &str) -> String {
let bytes = text.as_bytes();
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < bytes.len() {
if self.in_comment {
if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
self.in_comment = false;
i += 2;
} else {
i += 1;
}
continue;
}
match bytes[i] {
b'/' if bytes.get(i + 1) == Some(&b'*') => {
self.in_comment = true;
i += 2;
push_space(&mut out);
}
b'/' if bytes.get(i + 1) == Some(&b'/') => break,
b'"' | b'\'' => {
let quote = bytes[i];
match literal(bytes, i) {
Some(end) => {
out.push_str(&text[i..end]);
i = end;
}
None => {
out.push(quote as char);
i += 1;
}
}
}
b' ' | b'\t' | b'\r' | b'\n' => {
push_space(&mut out);
i += 1;
}
b'\\' if i + 1 == bytes.len() || bytes[i + 1] == b'\n' || bytes[i + 1] == b'\r' => {
push_space(&mut out);
i += 1;
}
byte => {
out.push(byte as char);
i += 1;
}
}
}
out.trim().to_owned()
}
}
fn push_space(out: &mut String) {
if !out.is_empty() && !out.ends_with(' ') {
out.push(' ');
}
}
fn literal(bytes: &[u8], at: usize) -> Option<usize> {
let quote = bytes[at];
let mut i = at + 1;
while i < bytes.len() {
match bytes[i] {
b'\\' => i += 2,
b'\n' => return None,
byte if byte == quote => return Some(i + 1),
_ => i += 1,
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn rejoined(text: &str) -> String {
let cut = pieces(text);
let mut out: String = cut.items.iter().map(|i| i.text.as_str()).collect();
out.push_str(&cut.tail);
out
}
#[test]
fn a_file_comes_back_out_of_its_pieces() {
for text in [
"#define A 1\n",
"/* one */\n#define A 1\n/* two */\n",
"#define A \\\n 1\n",
"no newline at the end",
"\n\n",
"",
] {
assert_eq!(rejoined(text), text, "{text:?}");
}
}
#[test]
fn a_comment_above_a_declaration_belongs_to_it() {
let cut = pieces("/* what it does\n and why */\nint f (void);\n#define A 1\n");
assert_eq!(cut.items.len(), 2);
assert!(cut.items[0].text.starts_with("/* what it does"));
assert_eq!(&cut.items[0].text[cut.items[0].code_at..], "int f (void);\n");
assert_eq!(cut.items[0].key, "int f (void);");
assert_eq!(cut.items[1].key, "#define A 1");
assert_eq!(cut.tail, "");
}
#[test]
fn the_comments_at_the_end_belong_to_nothing() {
let cut = pieces("int f (void);\n/* the end */\n");
assert_eq!(cut.items.len(), 1);
assert_eq!(cut.tail, "/* the end */\n");
}
#[test]
fn a_continued_macro_is_one_piece() {
let cut = pieces("#define TWO(a, b) \\\n do { a; b; } while (0)\n#define A 1\n");
assert_eq!(cut.items.len(), 2);
assert_eq!(cut.items[0].key, "#define TWO(a, b) do { a; b; } while (0)");
assert_eq!(cut.items[1].key, "#define A 1");
}
#[test]
fn the_year_in_the_copyright_line_is_not_code() {
let old = "/* Copyright (C) 1991-2018 Free Software Foundation, Inc.\n http://x */\n\
#define A 1\n";
let new = "/* Copyright (C) 1991-2024 Free Software Foundation, Inc.\n https://x */\n\
#define A 1\n";
assert_ne!(old, new);
assert_eq!(code(old), code(new));
assert_eq!(code(old), "#define A 1");
}
#[test]
fn a_comment_between_two_tokens_is_a_space() {
assert_eq!(code("int/* and */f (void);\n"), "int f (void);");
assert_eq!(code("int \tf (void);\n"), "int f (void);");
}
#[test]
fn a_comment_start_inside_a_string_is_not_one() {
assert_eq!(code("#define S \"/*\"\n#define A 1\n"), "#define S \"/*\"\n#define A 1");
}
#[test]
fn an_apostrophe_in_a_comment_does_not_eat_the_file() {
let text = "/* The macros `__GLIBC__' and `__GLIBC_MINOR__' are defined. */\n#define A 1\n";
assert_eq!(code(text), "#define A 1");
}
#[test]
fn a_line_comment_ends_the_code_on_its_line() {
assert_eq!(
code("#define A 1 // and nothing after\n#define B 2\n"),
"#define A 1\n#define B 2"
);
}
#[test]
fn a_comment_that_spans_lines_is_gone_from_all_of_them() {
assert_eq!(
code("#define A 1\n/* one\n two\n three */\n#define B 2\n"),
"#define A 1\n#define B 2"
);
}
#[test]
fn a_comment_that_opens_after_code_keeps_its_piece_open() {
let text = "#define A 1\t/* one\n\t\t\t two. */\n#define B 2\n";
let cut = pieces(text);
assert_eq!(cut.items.len(), 2);
assert_eq!(cut.items[0].text, "#define A 1\t/* one\n\t\t\t two. */\n");
assert_eq!(cut.items[0].key, "#define A 1");
assert_eq!(cut.items[1].text, "#define B 2\n");
assert_eq!(rejoined(text), text);
}
#[test]
fn moving_where_a_continued_line_breaks_is_not_a_change() {
let old = "#define F(x) \\\n (a (x) ? b (x) : \\\n c (x))\n";
let new = "#define F(x) \\\n (a (x) ? b (x) \\\n : c (x))\n";
assert_eq!(code(old), code(new));
assert_eq!(code(old), "#define F(x) (a (x) ? b (x) : c (x))");
}
}