rust-type-sizes 0.0.2

parsing for '-Zprint-type-sizes'
Documentation
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Item {
    pub total: Field,
    pub kind: Kind,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Field {
    pub name: String,
    pub size: usize,
    pub alignment: usize,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum FieldOrPadding {
    Field(Field),
    Padding(usize),
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Variant {
    pub total: Field,
    pub fields: Vec<FieldOrPadding>,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Kind {
    EnumItem {
        discriminant: usize,
        variants: Vec<Variant>,
    },
    StructItem {
        fields: Vec<FieldOrPadding>,
    },
}

pub fn item_from_input(out: &str, skip: impl Fn(&str, usize) -> bool) -> Option<Item> {
    let Some(out) = out.strip_prefix("print-type-size type: `") else {
        panic!("did not start with 'print-type-size type: `'");
    };

    let (name, out) = out.split_once("`: ").expect("find name");
    let (size, out) = out.split_once(", alignment: ").unwrap_or(("", out));
    let mut rest = out.lines();
    let alignment = rest.next().unwrap();

    let size = size
        .strip_suffix(" bytes")
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);

    let alignment = alignment
        .strip_suffix(" bytes")
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);

    if skip(name, size) {
        return None;
    }

    let total = Field {
        name: name.to_owned(),
        size,
        alignment,
    };

    let mut variant = None;
    let mut kind = if let Some(line) = rest.next() {
        let line = line.strip_prefix("print-type-size     ").unwrap();
        if let Some(line) = line.strip_prefix("discriminant: ") {
            let bytes = line
                .strip_suffix(" bytes")
                .unwrap()
                .parse()
                .expect("invalid byte count");
            Kind::EnumItem {
                discriminant: bytes,
                variants: Vec::new(),
            }
        } else if let Some(line) = line.strip_prefix("variant `") {
            let (name, size) = line.split_once("`: ").unwrap();
            let size = size
                .strip_suffix(" bytes")
                .unwrap()
                .parse()
                .expect("invalid byte count");
            let alignment = 0;
            let total = Field {
                name: name.to_owned(),
                size,
                alignment,
            };
            variant = Some(Variant {
                total,
                fields: Vec::new(),
            });
            Kind::EnumItem {
                discriminant: 0,
                variants: Vec::new(),
            }
        } else {
            let Some(line) = line.strip_prefix("field `.") else {
                panic!("{line:?}")
            };
            let (name, size) = line.split_once("`: ").unwrap();
            let size = size
                .strip_suffix(" bytes")
                .unwrap()
                .parse()
                .expect("invalid byte count");
            let alignment = 0;
            let field = Field {
                name: name.to_owned(),
                size,
                alignment,
            };
            Kind::StructItem {
                fields: vec![FieldOrPadding::Field(field)],
            }
        }
    } else {
        Kind::StructItem {
            fields: Vec::default(),
        }
    };

    match kind {
        Kind::StructItem { ref mut fields, .. } => {
            for line in rest {
                let Some(line) = line.strip_prefix("print-type-size     ") else {
                    break;
                };
                let field = if let Some(line) = line.strip_prefix("field `.") {
                    let (name, size) = line.split_once("`: ").unwrap();
                    let size = parse_byte_literal(size);
                    let alignment = 0;
                    let field = Field {
                        name: name.to_owned(),
                        size,
                        alignment,
                    };
                    FieldOrPadding::Field(field)
                } else if let Some(size) = line
                    .strip_prefix("end padding: ")
                    .or_else(|| line.strip_prefix("padding: "))
                {
                    let size = parse_byte_literal(size);
                    FieldOrPadding::Padding(size)
                } else {
                    eprintln!("unknown {line:?}");
                    continue;
                };
                fields.push(field);
            }
        }
        Kind::EnumItem {
            ref mut variants, ..
        } => {
            let mut variant = variant;
            for line in rest {
                let Some(line) = line.strip_prefix("print-type-size     ") else {
                    break;
                };
                if let Some(line) = line.strip_prefix("variant `") {
                    if let Some(variant) = variant.take() {
                        variants.push(variant);
                    }
                    let (name, size) = line.split_once("`: ").unwrap();
                    let size = parse_byte_literal(size);
                    let alignment = 0;
                    let total = Field {
                        name: name.to_owned(),
                        size,
                        alignment,
                    };
                    variant = Some(Variant {
                        total,
                        fields: Vec::new(),
                    });
                } else if let Some(line) = line.strip_prefix("    field `.") {
                    let (name, item) = line.split_once("`: ").unwrap();
                    let (item, offset) = item.split_once(", offset: ").unwrap_or((item, ""));
                    let (size, alignment) = item.split_once(", alignment: ").unwrap_or((item, ""));
                    let size = parse_byte_literal(size);
                    let alignment = if !alignment.is_empty() {
                        parse_byte_literal(alignment)
                    } else {
                        0
                    };
                    let _offset = if !offset.is_empty() {
                        parse_byte_literal(offset)
                    } else {
                        0
                    };
                    let field = Field {
                        name: name.to_owned(),
                        size,
                        alignment,
                    };
                    variant
                        .as_mut()
                        .unwrap()
                        .fields
                        .push(FieldOrPadding::Field(field));
                } else if let Some(line) = line
                    .strip_prefix("    end padding: ")
                    .or_else(|| line.strip_prefix("    padding: "))
                    .or_else(|| line.strip_prefix("end padding: "))
                {
                    // TODO discern cases?
                    let size = parse_byte_literal(line);
                    variant
                        .as_mut()
                        .unwrap()
                        .fields
                        .push(FieldOrPadding::Padding(size));
                } else {
                    eprintln!("enum todo {line:?}");
                    continue;
                }
            }
            if let Some(variant) = variant.take() {
                variants.push(variant);
            }
        }
    }

    Some(Item { total, kind })
}

fn parse_byte_literal(on: &str) -> usize {
    if let Some(size) = on.strip_suffix(" bytes") {
        parse_byte_count(size)
    } else {
        eprintln!("invalid byte name {on:?}");
        0
    }
}

fn parse_byte_count(on: &str) -> usize {
    if let Ok(size) = on.parse() {
        size
    } else {
        eprintln!("invalid byte count {on:?}");
        0
    }
}

#[cfg(test)]
mod tests {
    use super::{Field, FieldOrPadding, Kind, item_from_input};

    #[test]
    fn parse() {
        let out = &["print-type-size type: `Item<'_, '_>`: 48 bytes, alignment: 8 bytes
print-type-size     field `.total_source`: 16 bytes
print-type-size     field `.raw_node`: 32 bytes",

    "print-type-size type: `tree_sitter::Node<'_>`: 32 bytes, alignment: 8 bytes
print-type-size     field `.0`: 32 bytes
print-type-size     field `.1`: 0 bytes",

    "print-type-size type: `result::Result<(), std::fmt::Error>`: 1 bytes, alignment: 1 bytes
print-type-size     discriminant: 1 bytes
print-type-size     variant `Ok`: 0 bytes
print-type-size         field `.0`: 0 bytes
print-type-size     variant `Err`: 0 bytes
print-type-size         field `.0`: 0 bytes",

    "print-type-size type: `result::Result<std::fs::ReadDir, std::io::Error>`: 624 bytes, alignment: 8 bytes
print-type-size     variant `Ok`: 624 bytes
print-type-size         field `.0`: 624 bytes
print-type-size     variant `Err`: 16 bytes
print-type-size         padding: 8 bytes
print-type-size         field `.0`: 8 bytes, alignment: 8 bytes",

    "print-type-size type: `sys::fs::windows::ReadDir`: 624 bytes, alignment: 8 bytes
print-type-size     field `.handle`: 16 bytes
print-type-size     field `.root`: 8 bytes
print-type-size     field `.first`: 596 bytes
print-type-size     end padding: 4 bytes",

    "print-type-size type: `ffi::c_void`: 1 bytes, alignment: 1 bytes
print-type-size     discriminant: 1 bytes
print-type-size     variant `__variant1`: 0 bytes
print-type-size     variant `__variant2`: 0 bytes"];

        let items: Vec<_> = out
            .iter()
            .map(|part| item_from_input(part, |_, _| false).unwrap())
            .collect();

        assert_eq!(&items[0].total.name, "Item<'_, '_>");
        assert_eq!(items[0].total.size, 48);

        assert_eq!(items[4].total.name, "sys::fs::windows::ReadDir");
        assert_eq!(items[4].total.size, 624);
        assert_eq!(
            items[4].kind,
            Kind::StructItem {
                fields: vec![
                    FieldOrPadding::Field(Field {
                        name: "handle".to_owned(),
                        size: 16,
                        alignment: 0
                    }),
                    FieldOrPadding::Field(Field {
                        name: "root".to_owned(),
                        size: 8,
                        alignment: 0
                    }),
                    FieldOrPadding::Field(Field {
                        name: "first".to_owned(),
                        size: 596,
                        alignment: 0
                    }),
                    FieldOrPadding::Padding(4),
                ]
            }
        );
    }
}