use std::path::PathBuf;
use syn::visit::Visit;
use syn::{Expr, File, Item, ItemConst, ItemMod, ItemStatic};
use crate::models::Visibility;
#[derive(Debug, Clone)]
pub struct FoundConstant {
pub name: String,
pub value: Option<String>,
pub is_static: bool,
pub is_mutable: bool,
pub visibility: Visibility,
pub line: u32,
pub module_path: Vec<String>,
}
pub struct ConstantFinder {
constants: Vec<FoundConstant>,
module_path: Vec<String>,
file_path: PathBuf,
}
impl ConstantFinder {
pub fn new(file_path: PathBuf) -> Self {
Self {
constants: Vec::new(),
module_path: Vec::new(),
file_path,
}
}
pub fn find(file_path: PathBuf, file: &File) -> Vec<FoundConstant> {
let mut finder = Self::new(file_path);
finder.visit_file(file);
finder.constants
}
pub fn extract_string_value(expr: &Expr) -> Option<String> {
match expr {
Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::ByteStr(s) => String::from_utf8(s.value()).ok(),
_ => None,
},
Expr::Reference(r) => Self::try_decode_referenced_value(&r.expr),
Expr::Macro(mac) => {
if mac
.mac
.path
.segments
.last()
.map(|s| s.ident == "vec")
.unwrap_or(false)
{
Self::try_decode_vec_macro(&mac.mac.tokens)
} else {
None
}
}
Expr::Call(call) => {
let func_name = quote::quote!(#call.func).to_string();
if func_name.contains("from_utf8") {
call.args.first().and_then(Self::extract_string_value)
} else {
None
}
}
Expr::Group(g) => Self::extract_string_value(&g.expr),
Expr::Paren(p) => Self::extract_string_value(&p.expr),
_ => None,
}
}
fn try_decode_referenced_value(expr: &Expr) -> Option<String> {
if let Expr::Array(arr) = expr {
let bytes: Option<Vec<u8>> = arr
.elems
.iter()
.map(|e| {
if let Expr::Lit(lit) = e {
match &lit.lit {
syn::Lit::Int(i) => i.base10_parse::<u8>().ok(),
syn::Lit::Byte(b) => Some(b.value()),
_ => None,
}
} else {
None
}
})
.collect();
if let Some(bytes) = bytes {
if let Ok(s) = String::from_utf8(bytes) {
return Some(s);
}
}
let chars: Option<String> = arr
.elems
.iter()
.map(|e| {
if let Expr::Lit(lit) = e {
if let syn::Lit::Char(c) = &lit.lit {
return Some(c.value());
}
}
None
})
.collect();
return chars;
}
None
}
fn try_decode_vec_macro(tokens: &proc_macro2::TokenStream) -> Option<String> {
use syn::parse::Parser;
use syn::{Expr, ExprLit, Lit};
let parser = syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated;
let exprs: syn::punctuated::Punctuated<Expr, syn::Token![,]> =
match parser.parse2(tokens.clone()) {
Ok(exprs) => exprs,
Err(_) => return None,
};
let bytes: Option<Vec<u8>> = exprs
.iter()
.map(|expr| {
if let Expr::Lit(ExprLit {
lit: Lit::Int(int), ..
}) = expr
{
int.base10_parse::<u8>().ok()
} else if let Expr::Lit(ExprLit {
lit: Lit::Byte(b), ..
}) = expr
{
Some(b.value())
} else {
None
}
})
.collect();
bytes.and_then(|b| String::from_utf8(b).ok())
}
pub fn constants(&self) -> &[FoundConstant] {
&self.constants
}
pub fn into_constants(self) -> Vec<FoundConstant> {
self.constants
}
}
impl<'ast> Visit<'ast> for ConstantFinder {
fn visit_item_const(&mut self, node: &'ast ItemConst) {
let value = Self::extract_string_value(&node.expr);
let visibility = Visibility::from_syn(&node.vis);
self.constants.push(FoundConstant {
name: node.ident.to_string(),
value,
is_static: false,
is_mutable: false,
visibility,
line: node.ident.span().start().line as u32,
module_path: self.module_path.clone(),
});
syn::visit::visit_item_const(self, node);
}
fn visit_item_static(&mut self, node: &'ast ItemStatic) {
let value = Self::extract_string_value(&node.expr);
let visibility = Visibility::from_syn(&node.vis);
self.constants.push(FoundConstant {
name: node.ident.to_string(),
value,
is_static: true,
is_mutable: !matches!(node.mutability, syn::StaticMutability::None),
visibility,
line: node.ident.span().start().line as u32,
module_path: self.module_path.clone(),
});
syn::visit::visit_item_static(self, node);
}
fn visit_item_mod(&mut self, node: &'ast ItemMod) {
let name = node.ident.to_string();
self.module_path.push(name);
syn::visit::visit_item_mod(self, node);
self.module_path.pop();
}
fn visit_item(&mut self, item: &'ast Item) {
match item {
Item::Impl(_) => {}
_ => syn::visit::visit_item(self, item),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_const() {
let code = r#"
const API_KEY: &str = "secret123";
const NUMBER: i32 = 42;
"#;
let file: syn::File = syn::parse_str(code).unwrap();
let constants = ConstantFinder::find(PathBuf::from("test.rs"), &file);
assert_eq!(constants.len(), 2);
assert_eq!(constants[0].name, "API_KEY");
assert_eq!(constants[0].value, Some("secret123".to_string()));
assert!(!constants[0].is_static);
}
#[test]
fn test_find_static() {
let code = r#"
static DB_URL: &str = "localhost";
static mut COUNTER: i32 = 0;
"#;
let file: syn::File = syn::parse_str(code).unwrap();
let constants = ConstantFinder::find(PathBuf::from("test.rs"), &file);
assert_eq!(constants.len(), 2);
assert_eq!(constants[0].name, "DB_URL");
assert!(constants[0].is_static);
assert!(!constants[0].is_mutable);
assert!(constants[1].is_mutable);
}
#[test]
fn test_module_path() {
let code = r#"
mod outer {
const A: &str = "a";
mod inner {
const B: &str = "b";
}
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
let constants = ConstantFinder::find(PathBuf::from("test.rs"), &file);
assert_eq!(constants.len(), 2);
assert_eq!(constants[0].module_path, vec!["outer"]);
assert_eq!(constants[1].module_path, vec!["outer", "inner"]);
}
#[test]
fn test_visibility() {
let code = r#"
const PRIVATE: &str = "a";
pub const PUBLIC: &str = "b";
pub(crate) const PUB_CRATE: &str = "c";
"#;
let file: syn::File = syn::parse_str(code).unwrap();
let constants = ConstantFinder::find(PathBuf::from("test.rs"), &file);
assert_eq!(constants.len(), 3);
assert_eq!(constants[0].visibility, Visibility::Private);
assert_eq!(constants[1].visibility, Visibility::Public);
assert_eq!(constants[2].visibility, Visibility::PubCrate);
}
}