use compact_str::{CompactString, ToCompactString};
use derive_more::Deref;
use miette::SourceSpan;
use crate::{Id, Refer, SrcRef, SrcReferrer, TreeDisplay, TreeState};
#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Identifier(pub Refer<Id>);
impl SrcReferrer for Identifier {
fn src_ref(&self) -> SrcRef {
self.0.src_ref.clone()
}
}
impl Identifier {
pub fn none() -> Self {
Self(Refer::none("".into()))
}
pub fn unique() -> Self {
let mut num = UNIQUE_ID_NEXT
.lock()
.expect("lock on UNIQUE_ID_NEXT failed");
let id = format!("${num}");
*num += 1;
Identifier::no_ref(&id)
}
pub fn ignore(&self) -> bool {
self.0.starts_with("_")
}
pub fn is_super(&self) -> bool {
*self.0 == "super"
}
pub fn no_ref(id: &str) -> Self {
Self(Refer::none(id.into()))
}
pub fn id(&self) -> &Id {
&self.0.value
}
pub fn short_id(&self) -> ShortId {
let parts = self
.0
.value
.split("_")
.map(|part| {
part.chars()
.next()
.expect("cannot shorten empty Identifier")
})
.map(|p| p.to_compact_string())
.collect::<Vec<_>>()
.join("_")
.to_compact_string();
ShortId(parts)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn is_valid(&self) -> bool {
let str = self.0.as_str();
let Some(start) = str.chars().next() else {
return false;
};
(start == '_' || start.is_ascii_alphabetic())
&& str.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn detect_case(&self) -> Case {
let s = &self.0.value;
if s.is_empty() {
return Case::Invalid;
}
if s.len() == 1 {
let c = s.chars().next().expect("At least one char");
if c.is_ascii_uppercase() {
return Case::UpperSingleChar;
} else {
return Case::Invalid;
}
}
let has_underscore = s.contains('_');
if has_underscore {
if s.chars().all(|c| c.is_ascii_uppercase() || c == '_') {
return Case::UpperSnake;
} else if s.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
return Case::LowerSnake;
} else {
return Case::Invalid;
}
} else {
let mut chars = s.chars();
if let Some(first) = chars.next() {
if first.is_ascii_uppercase() && chars.all(|c| c.is_ascii_alphanumeric()) {
return Case::Pascal;
}
}
}
Case::Invalid
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Case {
Pascal,
LowerSnake,
UpperSnake,
UpperSingleChar,
Invalid,
}
#[derive(Deref)]
pub struct ShortId(CompactString);
impl PartialEq<Identifier> for ShortId {
fn eq(&self, other: &Identifier) -> bool {
self.0 == other.to_string()
}
}
impl From<Identifier> for SourceSpan {
fn from(value: Identifier) -> Self {
value.src_ref().into()
}
}
impl std::hash::Hash for Identifier {
fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
self.0.hash(hasher)
}
}
impl From<&std::ffi::OsStr> for Identifier {
fn from(value: &std::ffi::OsStr) -> Self {
Identifier::no_ref(value.to_string_lossy().to_string().as_str())
}
}
impl From<&str> for Identifier {
fn from(value: &str) -> Self {
let identifier = Identifier::no_ref(value);
assert!(identifier.is_valid());
identifier
}
}
impl<'a> From<&'a Identifier> for &'a str {
fn from(value: &'a Identifier) -> Self {
&value.0
}
}
impl std::fmt::Display for Identifier {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.is_empty() {
write!(f, "<NO ID>")
} else {
write!(f, "{}", self.0)
}
}
}
impl PartialEq<str> for Identifier {
fn eq(&self, other: &str) -> bool {
*self.0 == other
}
}
impl TreeDisplay for Identifier {
fn tree_print(&self, f: &mut std::fmt::Formatter, depth: TreeState) -> std::fmt::Result {
writeln!(f, "{:depth$}Identifier: {}", "", self.id())
}
}
static UNIQUE_ID_NEXT: std::sync::Mutex<usize> = std::sync::Mutex::new(0);
#[test]
fn identifier_comparison() {
use crate::{LineCol, SrcRef};
let id1 = Identifier::no_ref("x");
let id2 = Identifier(Refer::new(
"x".into(),
SrcRef::new(0..5, LineCol { line: 0, col: 1 }, 1),
));
assert!(id1 == id2);
}
#[test]
fn identifier_hash() {
use crate::{LineCol, SrcRef};
use std::hash::{Hash, Hasher};
let id1 = Identifier(Refer::none("x".into()));
let id2 = Identifier(Refer::new(
"x".into(),
SrcRef::new(0..5, LineCol { line: 0, col: 1 }, 1),
));
let mut hasher = std::hash::DefaultHasher::new();
id1.hash(&mut hasher);
let hash1 = hasher.finish();
let mut hasher = std::hash::DefaultHasher::new();
id2.hash(&mut hasher);
let hash2 = hasher.finish();
assert_eq!(hash1, hash2);
}
#[test]
fn identifier_case() {
let detect_case = |s| -> Case { Identifier::no_ref(s).detect_case() };
assert_eq!(detect_case("PascalCase"), Case::Pascal);
assert_eq!(detect_case("lower_snake_case"), Case::LowerSnake);
assert_eq!(detect_case("UPPER_SNAKE_CASE"), Case::UpperSnake);
assert_eq!(detect_case("notValid123_"), Case::Invalid);
assert_eq!(detect_case(""), Case::Invalid);
assert_eq!(detect_case("A"), Case::UpperSingleChar); assert_eq!(detect_case("z"), Case::Invalid); assert_eq!(detect_case("_"), Case::Invalid); assert_eq!(detect_case("a_b"), Case::LowerSnake);
assert_eq!(detect_case("A_B"), Case::UpperSnake);
println!("All tests passed.");
}
#[test]
fn test_short_identifiers() {
fn test(id: &str) -> String {
Identifier::from(id).short_id().to_string()
}
assert_eq!(test("weather_thermal_function"), "w_t_f");
assert_eq!(test("width"), "w");
assert_eq!(test("WeatherThermal_Function"), "W_F");
}