1pub fn to_snake(ident: &str) -> String {
9 let chars: Vec<char> = ident.chars().collect();
10 let mut out = String::with_capacity(ident.len() + 4);
11 for (i, &c) in chars.iter().enumerate() {
12 if c == '_' {
13 if !out.ends_with('_') && !out.is_empty() {
15 out.push('_');
16 }
17 continue;
18 }
19 if c.is_ascii_uppercase() {
20 let prev = if i > 0 { Some(chars[i - 1]) } else { None };
21 let next = chars.get(i + 1).copied();
22 let boundary = match prev {
23 None => false,
24 Some('_') => false,
25 Some(p) if p.is_ascii_lowercase() || p.is_ascii_digit() => true,
27 Some(p) if p.is_ascii_uppercase() => {
29 matches!(next, Some(n) if n.is_ascii_lowercase())
30 }
31 _ => false,
32 };
33 if boundary && !out.is_empty() && !out.ends_with('_') {
34 out.push('_');
35 }
36 out.push(c.to_ascii_lowercase());
37 } else {
38 out.push(c);
39 }
40 }
41 let trimmed = out.trim_matches('_').to_string();
42 let stem = if trimmed.is_empty() { "item".to_string() } else { trimmed };
43 sanitize_stem(&stem)
44}
45
46const KEYWORDS: &[&str] = &[
48 "as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
49 "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
50 "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
51 "use", "where", "while", "async", "await", "abstract", "become", "box", "do", "final",
52 "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try", "gen",
53];
54
55fn sanitize_stem(stem: &str) -> String {
57 if KEYWORDS.contains(&stem) {
58 format!("{stem}_")
59 } else {
60 stem.to_string()
61 }
62}
63
64pub fn is_keyword(name: &str) -> bool {
66 KEYWORDS.contains(&name)
67}
68
69pub fn line_start(src: &str, byte: usize) -> usize {
71 src[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0)
72}
73
74pub fn leading_comment_start(src: &str, gap_start: usize, item_start: usize) -> usize {
82 let ls = line_start(src, item_start);
83 if ls <= gap_start {
84 return item_start;
85 }
86 let mut block_start = ls;
88 let mut cursor = ls;
89 loop {
90 if cursor <= gap_start {
91 break;
92 }
93 let prev_line_end = cursor - 1; let prev_line_start = src[gap_start..prev_line_end]
96 .rfind('\n')
97 .map(|i| gap_start + i + 1)
98 .unwrap_or(gap_start);
99 let line = src[prev_line_start..prev_line_end].trim();
100 let is_comment = line.starts_with("//") && !line.starts_with("///") && !line.starts_with("//!");
101 if is_comment || (line.starts_with("//") && prev_line_start >= gap_start) {
103 block_start = prev_line_start;
104 cursor = prev_line_start;
105 } else {
106 break;
107 }
108 }
109 if block_start < ls {
110 block_start
111 } else {
112 item_start
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn snake_cases() {
122 assert_eq!(to_snake("Foo"), "foo");
123 assert_eq!(to_snake("FooBar"), "foo_bar");
124 assert_eq!(to_snake("HTTPServer"), "http_server");
125 assert_eq!(to_snake("IOError"), "io_error");
126 assert_eq!(to_snake("MAX_SIZE"), "max_size");
127 assert_eq!(to_snake("parse_input"), "parse_input");
128 assert_eq!(to_snake("Http2Server"), "http2_server");
129 assert_eq!(to_snake("A"), "a");
130 assert_eq!(to_snake("VersionReq"), "version_req");
131 }
132
133 #[test]
134 fn keyword_stems_are_sanitized() {
135 assert_eq!(to_snake("Match"), "match_");
137 assert_eq!(to_snake("Type"), "type_");
138 assert!(!is_keyword("match_"));
139 }
140}
141
142pub fn extend_trailing_comment(src: &str, end: usize) -> usize {
145 let bytes = src.as_bytes();
146 let line_end = src[end..].find('\n').map(|i| end + i).unwrap_or(src.len());
147 let rest = &src[end..line_end];
148 let trimmed = rest.trim_start();
149 if trimmed.starts_with("//") {
150 let _ = bytes;
153 line_end
154 } else {
155 end
156 }
157}