1use bock_lexer::keyword_lookup;
16use thiserror::Error;
17
18#[derive(Debug, Clone, PartialEq, Eq, Error)]
20pub enum RenameError {
21 #[error("`{0}` is not a valid Bock identifier")]
23 InvalidIdentifier(String),
24 #[error("`{0}` is a reserved Bock keyword and cannot be used as a name")]
26 ReservedKeyword(String),
27 #[error(
32 "`{new}` does not match the capitalization of `{old}`: names starting \
33 with an uppercase letter are type identifiers, all others are value \
34 identifiers"
35 )]
36 CapitalizationMismatch {
37 old: String,
39 new: String,
41 },
42}
43
44pub fn validate_new_name(old_name: &str, new_name: &str) -> Result<(), RenameError> {
51 if !is_identifier_shaped(new_name) {
52 return Err(RenameError::InvalidIdentifier(new_name.to_string()));
53 }
54 if keyword_lookup(new_name).is_some() {
55 return Err(RenameError::ReservedKeyword(new_name.to_string()));
56 }
57 if starts_uppercase(old_name) != starts_uppercase(new_name) {
58 return Err(RenameError::CapitalizationMismatch {
59 old: old_name.to_string(),
60 new: new_name.to_string(),
61 });
62 }
63 Ok(())
64}
65
66fn is_identifier_shaped(name: &str) -> bool {
69 if name == "_" {
70 return false;
72 }
73 let mut chars = name.chars();
74 let Some(first) = chars.next() else {
75 return false;
76 };
77 if !(first.is_alphabetic() || first == '_') {
78 return false;
79 }
80 chars.all(|c| c.is_alphanumeric() || c == '_')
81}
82
83fn starts_uppercase(name: &str) -> bool {
86 name.starts_with(char::is_uppercase)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn accepts_simple_lowercase_rename() {
95 assert_eq!(validate_new_name("answer", "result"), Ok(()));
96 }
97
98 #[test]
99 fn accepts_underscores_and_digits() {
100 assert_eq!(validate_new_name("answer", "the_answer_42"), Ok(()));
101 assert_eq!(validate_new_name("answer", "_private"), Ok(()));
102 }
103
104 #[test]
105 fn accepts_uppercase_rename_for_uppercase_name() {
106 assert_eq!(validate_new_name("Point", "Coordinate"), Ok(()));
107 }
108
109 #[test]
110 fn rejects_keywords() {
111 for kw in ["fn", "let", "mut", "match", "if", "true", "false"] {
112 assert_eq!(
113 validate_new_name("answer", kw),
114 Err(RenameError::ReservedKeyword(kw.to_string())),
115 "`{kw}` must be rejected",
116 );
117 }
118 }
119
120 #[test]
121 fn rejects_constructor_keywords() {
122 for kw in ["Ok", "Err", "Some", "None", "Self"] {
125 assert_eq!(
126 validate_new_name("Point", kw),
127 Err(RenameError::ReservedKeyword(kw.to_string())),
128 "`{kw}` must be rejected",
129 );
130 }
131 }
132
133 #[test]
134 fn rejects_non_identifier_shapes() {
135 for bad in ["", "_", "123abc", "foo-bar", "foo bar", "a.b", "x!"] {
136 assert_eq!(
137 validate_new_name("answer", bad),
138 Err(RenameError::InvalidIdentifier(bad.to_string())),
139 "`{bad}` must be rejected",
140 );
141 }
142 }
143
144 #[test]
145 fn rejects_capitalization_class_change() {
146 assert!(matches!(
147 validate_new_name("Point", "point"),
148 Err(RenameError::CapitalizationMismatch { .. }),
149 ));
150 assert!(matches!(
151 validate_new_name("answer", "Answer"),
152 Err(RenameError::CapitalizationMismatch { .. }),
153 ));
154 }
155
156 #[test]
157 fn error_messages_are_descriptive() {
158 let err = validate_new_name("answer", "fn").expect_err("keyword rejected");
159 assert!(err.to_string().contains("reserved"), "got: {err}");
160 let err = validate_new_name("Point", "point").expect_err("case rejected");
161 assert!(err.to_string().contains("capitalization"), "got: {err}");
162 }
163}