#[cfg(feature = "no_std")]
use alloc::{format, string::String};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentKind {
Value,
Type,
Constant,
Wildcard,
}
pub fn classify(name: &str) -> IdentKind {
let core = name.trim_start_matches('_');
if core.is_empty() {
return IdentKind::Wildcard;
}
let first = core.as_bytes()[0];
if first.is_ascii_uppercase() {
if core.bytes().any(|b| b.is_ascii_lowercase()) {
IdentKind::Type
} else {
IdentKind::Constant
}
} else {
IdentKind::Value
}
}
pub fn is_private(name: &str) -> bool {
name.starts_with('_') && !name.trim_start_matches('_').is_empty()
}
pub fn is_value_name(name: &str) -> bool {
matches!(classify(name), IdentKind::Value | IdentKind::Wildcard)
}
pub fn is_type_name(name: &str) -> bool {
matches!(classify(name), IdentKind::Type | IdentKind::Constant)
}
pub fn is_constant_name(name: &str) -> bool {
matches!(classify(name), IdentKind::Constant)
}
pub fn kind_label(kind: IdentKind) -> &'static str {
match kind {
IdentKind::Value => "value",
IdentKind::Type => "type",
IdentKind::Constant => "constant",
IdentKind::Wildcard => "wildcard",
}
}
pub fn hint_for(expected_kind: &str, actual: &str) -> String {
match expected_kind {
"value" => {
let is_all_upper = actual.chars().all(|c| !c.is_ascii_lowercase());
if is_all_upper && actual.chars().any(|c| c.is_ascii_alphabetic()) {
return format!(
"names bound by `let` / `fn` / params start with a lowercase letter. \
Did you mean to declare a constant? (`const {} = ...`)",
actual
);
}
format!(
"names bound by `let` / `fn` / params start with a lowercase letter. \
Try `{}`?",
lowercase_first(actual)
)
}
"type" => {
format!(
"type names start with an uppercase letter. Try `{}`?",
capitalize_first(actual)
)
}
"constant" => {
format!(
"`const` names are SCREAMING_SNAKE_CASE (all uppercase). Try `{}`?",
all_caps(actual)
)
}
_ => String::new(),
}
}
fn capitalize_first(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut first = true;
for c in s.chars() {
if first {
out.extend(c.to_uppercase());
first = false;
} else {
out.push(c);
}
}
out
}
fn lowercase_first(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut first = true;
for c in s.chars() {
if first {
out.extend(c.to_lowercase());
first = false;
} else {
out.push(c);
}
}
out
}
fn all_caps(s: &str) -> String {
s.chars().flat_map(|c| c.to_uppercase()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_shapes() {
for name in [
"foo", "my_var", "camelCase", "doTheThing", "_foo", "_bar", "__baz", "x1", "_1",
] {
assert_eq!(classify(name), IdentKind::Value, "{name} should be Value");
}
}
#[test]
fn type_shapes_are_pascal_case() {
for name in [
"Entity", "Result", "Ok", "Err", "HttpClient", "_Internal", "Foo", "BarBaz",
] {
assert_eq!(classify(name), IdentKind::Type, "{name} should be Type");
}
}
#[test]
fn constant_shapes_are_all_caps() {
for name in [
"PI",
"MAX_SIZE",
"HTTP_PORT",
"HTTP",
"_DEBUG",
"__RESERVED",
"X",
"X2",
"X_Y",
"N",
] {
assert_eq!(
classify(name),
IdentKind::Constant,
"{name} should be Constant"
);
}
}
#[test]
fn wildcards() {
assert_eq!(classify("_"), IdentKind::Wildcard);
assert_eq!(classify("__"), IdentKind::Wildcard);
}
#[test]
fn type_sites_accept_both_type_and_constant_shapes() {
assert!(is_type_name("Foo"));
assert!(is_type_name("FOO"));
assert!(is_type_name("N"));
assert!(!is_type_name("foo"));
assert!(!is_type_name("_"));
}
#[test]
fn constant_sites_require_all_caps() {
assert!(is_constant_name("PI"));
assert!(is_constant_name("MAX"));
assert!(!is_constant_name("Pi"));
assert!(!is_constant_name("pi"));
}
#[test]
fn privacy_marker() {
assert!(is_private("_foo"));
assert!(is_private("_Foo"));
assert!(is_private("_FOO"));
assert!(!is_private("foo"));
assert!(!is_private("Foo"));
assert!(!is_private("FOO"));
assert!(!is_private("_")); }
}