#[cfg(feature = "literal-search")]
mod index;
#[cfg(feature = "literal-search")]
pub use index::{LiteralIndex, LiteralMatch, LiteralQuery, LiteralSearchError};
use crate::symbol::{FileId, SymbolId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LiteralKind {
String,
ByteStr,
Char,
Byte,
Int,
Float,
Bool,
Doc,
}
impl LiteralKind {
pub fn infer(s: &str) -> Self {
let s = s.trim();
if s.is_empty() {
return Self::String;
}
if s == "true" || s == "false" {
return Self::Bool;
}
if s.starts_with("b'") && s.ends_with('\'') {
return Self::Byte;
}
if s.starts_with('\'') && s.ends_with('\'') {
return Self::Char;
}
if s.starts_with("b\"") || s.starts_with("br\"") || s.starts_with("br#") {
return Self::ByteStr;
}
if s.starts_with('"') || s.starts_with("r\"") || s.starts_with("r#") {
return Self::String;
}
if Self::is_float_literal(s) {
return Self::Float;
}
Self::Int
}
fn is_float_literal(s: &str) -> bool {
let s = s.replace('_', "");
if s.ends_with("f32") || s.ends_with("f64") {
return true;
}
if s.starts_with("0x")
|| s.starts_with("0X")
|| s.starts_with("0b")
|| s.starts_with("0B")
|| s.starts_with("0o")
|| s.starts_with("0O")
{
return false;
}
s.contains('.') || s.contains('e') || s.contains('E')
}
pub fn as_str(&self) -> &'static str {
match self {
Self::String => "string",
Self::ByteStr => "bytestr",
Self::Char => "char",
Self::Byte => "byte",
Self::Int => "int",
Self::Float => "float",
Self::Bool => "bool",
Self::Doc => "doc",
}
}
pub fn parse_kind(s: &str) -> Option<Self> {
match s {
"string" => Some(Self::String),
"bytestr" => Some(Self::ByteStr),
"char" => Some(Self::Char),
"byte" => Some(Self::Byte),
"int" => Some(Self::Int),
"float" => Some(Self::Float),
"bool" => Some(Self::Bool),
"doc" => Some(Self::Doc),
_ => None,
}
}
}
impl std::fmt::Display for LiteralKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
fn extract_doc_content(value: &str) -> String {
let trimmed = value.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
let inner = &trimmed[1..trimmed.len() - 1];
inner.strip_prefix(' ').unwrap_or(inner).to_string()
} else {
trimmed.to_string()
}
}
fn extract_string_literals_from_macro_tokens(tokens: &str) -> Vec<String> {
let mut results = Vec::new();
let chars: Vec<char> = tokens.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if chars[i] == 'r' && i + 1 < len {
let mut hashes = 0;
let mut j = i + 1;
while j < len && chars[j] == '#' {
hashes += 1;
j += 1;
}
if j < len && chars[j] == '"' {
let start = i;
j += 1; loop {
if j >= len {
break;
}
if chars[j] == '"' {
let mut closing_hashes = 0;
let mut k = j + 1;
while k < len && chars[k] == '#' && closing_hashes < hashes {
closing_hashes += 1;
k += 1;
}
if closing_hashes == hashes {
let literal: String = chars[start..k].iter().collect();
results.push(literal);
i = k;
break;
}
}
j += 1;
}
i += 1;
continue;
}
}
if chars[i] == '"' {
let start = i;
i += 1;
while i < len {
if chars[i] == '\\' && i + 1 < len {
i += 2;
} else if chars[i] == '"' {
let literal: String = chars[start..=i].iter().collect();
results.push(literal);
i += 1;
break;
} else {
i += 1;
}
}
continue;
}
i += 1;
}
results
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiteralInfo {
pub value: String,
pub kind: LiteralKind,
pub symbol_id: SymbolId,
pub file_id: FileId,
}
impl LiteralInfo {
pub fn new(value: String, symbol_id: SymbolId, file_id: FileId) -> Self {
let kind = LiteralKind::infer(&value);
Self {
value,
kind,
symbol_id,
file_id,
}
}
pub fn with_kind(
value: String,
kind: LiteralKind,
symbol_id: SymbolId,
file_id: FileId,
) -> Self {
Self {
value,
kind,
symbol_id,
file_id,
}
}
pub fn inner_value(&self) -> &str {
let s = self.value.trim();
if s.starts_with("r#") || s.starts_with("br#") {
if let Some(start) = s.find('"') {
if let Some(end) = s.rfind('"') {
if end > start {
return &s[start + 1..end];
}
}
}
return s;
}
if s.starts_with("b\"") && s.ends_with('"') && s.len() > 3 {
return &s[2..s.len() - 1];
}
if s.starts_with('"') && s.ends_with('"') && s.len() > 1 {
return &s[1..s.len() - 1];
}
if s.starts_with('\'') && s.ends_with('\'') && s.len() > 2 {
return &s[1..s.len() - 1];
}
s
}
}
use ryo_source::pure::{
PureAttrMeta, PureAttribute, PureBlock, PureConst, PureExpr, PureFields, PureFile, PureFn,
PureImpl, PureImplItem, PureItem, PureMatchArm, PurePattern, PureStatic, PureStmt,
};
pub struct LiteralCollector;
impl LiteralCollector {
pub fn collect_file<F>(
file: &PureFile,
file_id: FileId,
default_symbol: SymbolId,
symbol_resolver: impl Fn(&str) -> Option<SymbolId>,
mut callback: F,
) where
F: FnMut(LiteralInfo),
{
Self::collect_doc_comments(&file.attrs, file_id, default_symbol, &mut callback);
for item in &file.items {
Self::collect_item(
item,
file_id,
default_symbol,
&symbol_resolver,
&mut callback,
);
}
}
fn collect_doc_comments<F>(
attrs: &[PureAttribute],
file_id: FileId,
symbol_id: SymbolId,
callback: &mut F,
) where
F: FnMut(LiteralInfo),
{
for attr in attrs {
if attr.path == "doc" {
if let PureAttrMeta::NameValue(value) = &attr.meta {
let doc_content = extract_doc_content(value);
callback(LiteralInfo::with_kind(
doc_content,
LiteralKind::Doc,
symbol_id,
file_id,
));
}
}
}
}
fn collect_item<F>(
item: &PureItem,
file_id: FileId,
default_symbol: SymbolId,
symbol_resolver: &impl Fn(&str) -> Option<SymbolId>,
callback: &mut F,
) where
F: FnMut(LiteralInfo),
{
match item {
PureItem::Fn(f) => {
let symbol_id = symbol_resolver(&f.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&f.attrs, file_id, symbol_id, callback);
Self::collect_fn(f, file_id, symbol_id, callback);
}
PureItem::Struct(s) => {
let symbol_id = symbol_resolver(&s.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&s.attrs, file_id, symbol_id, callback);
if let PureFields::Named(fields) = &s.fields {
for field in fields {
Self::collect_doc_comments(&field.attrs, file_id, symbol_id, callback);
}
}
}
PureItem::Enum(e) => {
let symbol_id = symbol_resolver(&e.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&e.attrs, file_id, symbol_id, callback);
for variant in &e.variants {
Self::collect_doc_comments(&variant.attrs, file_id, symbol_id, callback);
}
}
PureItem::Trait(t) => {
let symbol_id = symbol_resolver(&t.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&t.attrs, file_id, symbol_id, callback);
}
PureItem::Impl(i) => {
Self::collect_impl(i, file_id, default_symbol, symbol_resolver, callback);
}
PureItem::Const(c) => {
let symbol_id = symbol_resolver(&c.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&c.attrs, file_id, symbol_id, callback);
Self::collect_const(c, file_id, symbol_id, callback);
}
PureItem::Static(s) => {
let symbol_id = symbol_resolver(&s.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&s.attrs, file_id, symbol_id, callback);
Self::collect_static(s, file_id, symbol_id, callback);
}
PureItem::Mod(m) => {
let symbol_id = symbol_resolver(&m.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&m.attrs, file_id, symbol_id, callback);
for inner_item in &m.items {
Self::collect_item(
inner_item,
file_id,
default_symbol,
symbol_resolver,
callback,
);
}
}
_ => {}
}
}
fn collect_fn<F>(func: &PureFn, file_id: FileId, symbol_id: SymbolId, callback: &mut F)
where
F: FnMut(LiteralInfo),
{
Self::collect_block(&func.body, file_id, symbol_id, callback);
}
fn collect_impl<F>(
impl_block: &PureImpl,
file_id: FileId,
default_symbol: SymbolId,
symbol_resolver: &impl Fn(&str) -> Option<SymbolId>,
callback: &mut F,
) where
F: FnMut(LiteralInfo),
{
Self::collect_doc_comments(&impl_block.attrs, file_id, default_symbol, callback);
for item in &impl_block.items {
match item {
PureImplItem::Fn(m) => {
let symbol_id = symbol_resolver(&m.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&m.attrs, file_id, symbol_id, callback);
Self::collect_fn(m, file_id, symbol_id, callback);
}
PureImplItem::Const(c) => {
let symbol_id = symbol_resolver(&c.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&c.attrs, file_id, symbol_id, callback);
if let Some(v) = &c.value {
Self::collect_expr(v, file_id, symbol_id, callback);
}
}
PureImplItem::Type(t) => {
let symbol_id = symbol_resolver(&t.name).unwrap_or(default_symbol);
Self::collect_doc_comments(&t.attrs, file_id, symbol_id, callback);
}
_ => {}
}
}
}
fn collect_const<F>(c: &PureConst, file_id: FileId, symbol_id: SymbolId, callback: &mut F)
where
F: FnMut(LiteralInfo),
{
if let Some(v) = &c.value {
Self::collect_expr(v, file_id, symbol_id, callback);
}
}
fn collect_static<F>(s: &PureStatic, file_id: FileId, symbol_id: SymbolId, callback: &mut F)
where
F: FnMut(LiteralInfo),
{
Self::collect_expr(&s.value, file_id, symbol_id, callback);
}
fn collect_block<F>(block: &PureBlock, file_id: FileId, symbol_id: SymbolId, callback: &mut F)
where
F: FnMut(LiteralInfo),
{
for stmt in &block.stmts {
Self::collect_stmt(stmt, file_id, symbol_id, callback);
}
}
fn collect_stmt<F>(stmt: &PureStmt, file_id: FileId, symbol_id: SymbolId, callback: &mut F)
where
F: FnMut(LiteralInfo),
{
match stmt {
PureStmt::Local {
init: Some(expr), ..
} => {
Self::collect_expr(expr, file_id, symbol_id, callback);
}
PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
Self::collect_expr(expr, file_id, symbol_id, callback);
}
PureStmt::Item(item) => {
Self::collect_item(item, file_id, symbol_id, &|_| Some(symbol_id), callback);
}
_ => {}
}
}
fn collect_expr<F>(expr: &PureExpr, file_id: FileId, symbol_id: SymbolId, callback: &mut F)
where
F: FnMut(LiteralInfo),
{
match expr {
PureExpr::Lit(value) => {
callback(LiteralInfo::new(value.clone(), symbol_id, file_id));
}
PureExpr::Binary { left, right, .. } => {
Self::collect_expr(left, file_id, symbol_id, callback);
Self::collect_expr(right, file_id, symbol_id, callback);
}
PureExpr::Unary { expr: inner, .. } => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Call { func, args } => {
Self::collect_expr(func, file_id, symbol_id, callback);
for arg in args {
Self::collect_expr(arg, file_id, symbol_id, callback);
}
}
PureExpr::MethodCall { receiver, args, .. } => {
Self::collect_expr(receiver, file_id, symbol_id, callback);
for arg in args {
Self::collect_expr(arg, file_id, symbol_id, callback);
}
}
PureExpr::Index { expr: arr, index } => {
Self::collect_expr(arr, file_id, symbol_id, callback);
Self::collect_expr(index, file_id, symbol_id, callback);
}
PureExpr::Field { expr: inner, .. } => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Cast { expr: inner, .. } => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Ref { expr: inner, .. } => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Block { block, .. } => {
Self::collect_block(block, file_id, symbol_id, callback);
}
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
Self::collect_expr(cond, file_id, symbol_id, callback);
Self::collect_block(then_branch, file_id, symbol_id, callback);
if let Some(else_expr) = else_branch {
Self::collect_expr(else_expr, file_id, symbol_id, callback);
}
}
PureExpr::Match {
expr: match_expr,
arms,
} => {
Self::collect_expr(match_expr, file_id, symbol_id, callback);
for arm in arms {
Self::collect_match_arm(arm, file_id, symbol_id, callback);
}
}
PureExpr::Loop { body, .. } => {
Self::collect_block(body, file_id, symbol_id, callback);
}
PureExpr::While { cond, body, .. } => {
Self::collect_expr(cond, file_id, symbol_id, callback);
Self::collect_block(body, file_id, symbol_id, callback);
}
PureExpr::For {
expr: iter_expr,
body,
..
} => {
Self::collect_expr(iter_expr, file_id, symbol_id, callback);
Self::collect_block(body, file_id, symbol_id, callback);
}
PureExpr::Closure { body, .. } => {
Self::collect_expr(body, file_id, symbol_id, callback);
}
PureExpr::Async { body, .. } => {
Self::collect_block(body, file_id, symbol_id, callback);
}
PureExpr::Unsafe(block) => {
Self::collect_block(block, file_id, symbol_id, callback);
}
PureExpr::Return(Some(inner))
| PureExpr::Break {
expr: Some(inner), ..
} => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Try(inner) | PureExpr::Await(inner) => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
Self::collect_expr(e, file_id, symbol_id, callback);
}
}
PureExpr::Repeat { expr: elem, len } => {
Self::collect_expr(elem, file_id, symbol_id, callback);
Self::collect_expr(len, file_id, symbol_id, callback);
}
PureExpr::Struct { fields, .. } => {
for (_, e) in fields {
Self::collect_expr(e, file_id, symbol_id, callback);
}
}
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
Self::collect_expr(s, file_id, symbol_id, callback);
}
if let Some(e) = end {
Self::collect_expr(e, file_id, symbol_id, callback);
}
}
PureExpr::Let { expr: inner, .. } => {
Self::collect_expr(inner, file_id, symbol_id, callback);
}
PureExpr::Macro { tokens, .. } => {
for literal in extract_string_literals_from_macro_tokens(tokens) {
callback(LiteralInfo::new(literal, symbol_id, file_id));
}
}
PureExpr::Path(_)
| PureExpr::Continue { .. }
| PureExpr::Return(None)
| PureExpr::Break { expr: None, .. }
| PureExpr::Other(_) => {}
}
}
fn collect_match_arm<F>(
arm: &PureMatchArm,
file_id: FileId,
symbol_id: SymbolId,
callback: &mut F,
) where
F: FnMut(LiteralInfo),
{
Self::collect_pattern(&arm.pattern, file_id, symbol_id, callback);
if let Some(ref guard) = arm.guard {
Self::collect_expr(guard, file_id, symbol_id, callback);
}
Self::collect_expr(&arm.body, file_id, symbol_id, callback);
}
fn collect_pattern<F>(
pattern: &PurePattern,
file_id: FileId,
symbol_id: SymbolId,
callback: &mut F,
) where
F: FnMut(LiteralInfo),
{
match pattern {
PurePattern::Lit(value) => {
callback(LiteralInfo::new(value.clone(), symbol_id, file_id));
}
PurePattern::Tuple(patterns)
| PurePattern::Or(patterns)
| PurePattern::Slice(patterns) => {
for p in patterns {
Self::collect_pattern(p, file_id, symbol_id, callback);
}
}
PurePattern::Struct { fields, .. } => {
for (_, p) in fields {
Self::collect_pattern(p, file_id, symbol_id, callback);
}
}
PurePattern::Ref { pattern: inner, .. } => {
Self::collect_pattern(inner, file_id, symbol_id, callback);
}
PurePattern::Range { start, end, .. } => {
if let Some(s) = start {
callback(LiteralInfo::new(s.clone(), symbol_id, file_id));
}
if let Some(e) = end {
callback(LiteralInfo::new(e.clone(), symbol_id, file_id));
}
}
PurePattern::Ident { .. }
| PurePattern::Wild
| PurePattern::Rest
| PurePattern::Path(_)
| PurePattern::Other(_) => {}
}
}
}
pub fn extract_literals_from_item(item: &PureItem) -> Vec<String> {
let mut literals = Vec::new();
collect_literals_from_item(item, &mut literals);
literals
}
fn collect_literals_from_item(item: &PureItem, literals: &mut Vec<String>) {
match item {
PureItem::Fn(f) => collect_literals_from_block(&f.body, literals),
PureItem::Const(c) => {
if let Some(v) = &c.value {
collect_literals_from_expr(v, literals);
}
}
PureItem::Static(s) => collect_literals_from_expr(&s.value, literals),
PureItem::Impl(i) => {
for impl_item in &i.items {
match impl_item {
PureImplItem::Fn(f) => collect_literals_from_block(&f.body, literals),
PureImplItem::Const(c) => {
if let Some(v) = &c.value {
collect_literals_from_expr(v, literals);
}
}
_ => {}
}
}
}
PureItem::Mod(m) => {
for inner in &m.items {
collect_literals_from_item(inner, literals);
}
}
_ => {}
}
}
fn collect_literals_from_block(block: &PureBlock, literals: &mut Vec<String>) {
for stmt in &block.stmts {
collect_literals_from_stmt(stmt, literals);
}
}
fn collect_literals_from_stmt(stmt: &PureStmt, literals: &mut Vec<String>) {
match stmt {
PureStmt::Local {
init: Some(expr), ..
} => collect_literals_from_expr(expr, literals),
PureStmt::Semi(expr) | PureStmt::Expr(expr) => collect_literals_from_expr(expr, literals),
PureStmt::Item(item) => collect_literals_from_item(item, literals),
_ => {}
}
}
fn collect_literals_from_expr(expr: &PureExpr, literals: &mut Vec<String>) {
match expr {
PureExpr::Lit(value) => literals.push(value.clone()),
PureExpr::Binary { left, right, .. } => {
collect_literals_from_expr(left, literals);
collect_literals_from_expr(right, literals);
}
PureExpr::Unary { expr: inner, .. } => collect_literals_from_expr(inner, literals),
PureExpr::Call { func, args } => {
collect_literals_from_expr(func, literals);
for arg in args {
collect_literals_from_expr(arg, literals);
}
}
PureExpr::MethodCall { receiver, args, .. } => {
collect_literals_from_expr(receiver, literals);
for arg in args {
collect_literals_from_expr(arg, literals);
}
}
PureExpr::Index { expr: arr, index } => {
collect_literals_from_expr(arr, literals);
collect_literals_from_expr(index, literals);
}
PureExpr::Field { expr: inner, .. } => collect_literals_from_expr(inner, literals),
PureExpr::Cast { expr: inner, .. } => collect_literals_from_expr(inner, literals),
PureExpr::Ref { expr: inner, .. } => collect_literals_from_expr(inner, literals),
PureExpr::Block { block, .. } => collect_literals_from_block(block, literals),
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
collect_literals_from_expr(cond, literals);
collect_literals_from_block(then_branch, literals);
if let Some(else_expr) = else_branch {
collect_literals_from_expr(else_expr, literals);
}
}
PureExpr::Match {
expr: match_expr,
arms,
} => {
collect_literals_from_expr(match_expr, literals);
for arm in arms {
collect_literals_from_pattern(&arm.pattern, literals);
if let Some(ref guard) = arm.guard {
collect_literals_from_expr(guard, literals);
}
collect_literals_from_expr(&arm.body, literals);
}
}
PureExpr::Loop { body, .. } => collect_literals_from_block(body, literals),
PureExpr::While { cond, body, .. } => {
collect_literals_from_expr(cond, literals);
collect_literals_from_block(body, literals);
}
PureExpr::For {
expr: iter_expr,
body,
..
} => {
collect_literals_from_expr(iter_expr, literals);
collect_literals_from_block(body, literals);
}
PureExpr::Closure { body, .. } => collect_literals_from_expr(body, literals),
PureExpr::Async { body, .. } => collect_literals_from_block(body, literals),
PureExpr::Unsafe(block) => collect_literals_from_block(block, literals),
PureExpr::Return(Some(inner))
| PureExpr::Break {
expr: Some(inner), ..
} => {
collect_literals_from_expr(inner, literals);
}
PureExpr::Try(inner) | PureExpr::Await(inner) => {
collect_literals_from_expr(inner, literals)
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
collect_literals_from_expr(e, literals);
}
}
PureExpr::Repeat { expr: elem, len } => {
collect_literals_from_expr(elem, literals);
collect_literals_from_expr(len, literals);
}
PureExpr::Struct { fields, .. } => {
for (_, e) in fields {
collect_literals_from_expr(e, literals);
}
}
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
collect_literals_from_expr(s, literals);
}
if let Some(e) = end {
collect_literals_from_expr(e, literals);
}
}
PureExpr::Let { expr: inner, .. } => collect_literals_from_expr(inner, literals),
PureExpr::Macro { tokens, .. } => {
for literal in extract_string_literals_from_macro_tokens(tokens) {
literals.push(literal);
}
}
_ => {}
}
}
fn collect_literals_from_pattern(pattern: &PurePattern, literals: &mut Vec<String>) {
match pattern {
PurePattern::Lit(value) => literals.push(value.clone()),
PurePattern::Tuple(patterns) | PurePattern::Or(patterns) | PurePattern::Slice(patterns) => {
for p in patterns {
collect_literals_from_pattern(p, literals);
}
}
PurePattern::Struct { fields, .. } => {
for (_, p) in fields {
collect_literals_from_pattern(p, literals);
}
}
PurePattern::Ref { pattern: inner, .. } => collect_literals_from_pattern(inner, literals),
PurePattern::Range { start, end, .. } => {
if let Some(s) = start {
literals.push(s.clone());
}
if let Some(e) = end {
literals.push(e.clone());
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_bool() {
assert_eq!(LiteralKind::infer("true"), LiteralKind::Bool);
assert_eq!(LiteralKind::infer("false"), LiteralKind::Bool);
assert_eq!(LiteralKind::infer(" true "), LiteralKind::Bool);
}
#[test]
fn test_infer_string() {
assert_eq!(LiteralKind::infer("\"hello\""), LiteralKind::String);
assert_eq!(LiteralKind::infer("\"\""), LiteralKind::String);
assert_eq!(LiteralKind::infer("\"hello world\""), LiteralKind::String);
assert_eq!(LiteralKind::infer("\"escape\\nseq\""), LiteralKind::String);
}
#[test]
fn test_infer_raw_string() {
assert_eq!(LiteralKind::infer(r#"r"raw""#), LiteralKind::String);
assert_eq!(LiteralKind::infer(r##"r#"raw"#"##), LiteralKind::String);
assert_eq!(LiteralKind::infer(r###"r##"raw"##"###), LiteralKind::String);
}
#[test]
fn test_infer_byte_string() {
assert_eq!(LiteralKind::infer("b\"bytes\""), LiteralKind::ByteStr);
assert_eq!(LiteralKind::infer("br\"raw bytes\""), LiteralKind::ByteStr);
assert_eq!(LiteralKind::infer("br#\"raw\"#"), LiteralKind::ByteStr);
}
#[test]
fn test_infer_char() {
assert_eq!(LiteralKind::infer("'a'"), LiteralKind::Char);
assert_eq!(LiteralKind::infer("'\\n'"), LiteralKind::Char);
assert_eq!(LiteralKind::infer("'\\u{1F600}'"), LiteralKind::Char);
}
#[test]
fn test_infer_byte() {
assert_eq!(LiteralKind::infer("b'x'"), LiteralKind::Byte);
assert_eq!(LiteralKind::infer("b'\\n'"), LiteralKind::Byte);
}
#[test]
fn test_infer_int() {
assert_eq!(LiteralKind::infer("42"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("0xFF"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("0xff"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("0b1010"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("0o755"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("1_000_000"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("42i32"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("42u64"), LiteralKind::Int);
}
#[test]
fn test_infer_float() {
assert_eq!(LiteralKind::infer("3.14"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("1e10"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("1E10"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("2.5e-3"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("1.0f32"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("1.0f64"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("1f32"), LiteralKind::Float);
assert_eq!(LiteralKind::infer("3.14_159"), LiteralKind::Float);
}
#[test]
fn test_infer_hex_with_e_is_int() {
assert_eq!(LiteralKind::infer("0xDEADBEEF"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("0xCAFE"), LiteralKind::Int);
assert_eq!(LiteralKind::infer("0x1e10"), LiteralKind::Int);
}
#[test]
fn test_kind_roundtrip() {
let kinds = [
LiteralKind::String,
LiteralKind::ByteStr,
LiteralKind::Char,
LiteralKind::Byte,
LiteralKind::Int,
LiteralKind::Float,
LiteralKind::Bool,
LiteralKind::Doc,
];
for kind in kinds {
let s = kind.as_str();
let parsed = LiteralKind::parse_kind(s);
assert_eq!(parsed, Some(kind), "Failed for {:?}", kind);
}
}
#[test]
fn test_literal_info_inner_value_string() {
let info = make_test_info("\"hello\"");
assert_eq!(info.inner_value(), "hello");
}
#[test]
fn test_literal_info_inner_value_empty_string() {
let info = make_test_info("\"\"");
assert_eq!(info.inner_value(), "");
}
#[test]
fn test_literal_info_inner_value_char() {
let info = make_test_info("'x'");
assert_eq!(info.inner_value(), "x");
}
#[test]
fn test_literal_info_inner_value_byte_string() {
let info = make_test_info("b\"bytes\"");
assert_eq!(info.inner_value(), "bytes");
}
#[test]
fn test_literal_info_inner_value_number() {
let info = make_test_info("42");
assert_eq!(info.inner_value(), "42");
}
#[test]
fn test_literal_info_inner_value_bool() {
let info = make_test_info("true");
assert_eq!(info.inner_value(), "true");
}
fn make_test_info(value: &str) -> LiteralInfo {
use slotmap::KeyData;
let symbol_id = SymbolId::from(KeyData::from_ffi(1));
let file_id = FileId::from(KeyData::from_ffi(1));
LiteralInfo::new(value.to_string(), symbol_id, file_id)
}
#[test]
fn test_collector_simple_function() {
let source = r#"
fn foo() {
let x = 42;
let s = "hello";
}
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 2);
assert!(literals.iter().any(|l| l.value == "42"));
assert!(literals.iter().any(|l| l.value == "\"hello\""));
}
#[test]
fn test_collector_const_static() {
let source = r#"
const MAX: i32 = 100;
static NAME: &str = "test";
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 2);
assert!(literals
.iter()
.any(|l| l.value == "100" && l.kind == LiteralKind::Int));
assert!(literals
.iter()
.any(|l| l.value == "\"test\"" && l.kind == LiteralKind::String));
}
#[test]
fn test_collector_nested_expressions() {
let source = r#"
fn foo() {
let x = 1 + 2 * 3;
let y = if true { "a" } else { "b" };
}
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 6);
}
#[test]
fn test_collector_match_expression() {
let source = r#"
fn foo() {
match x {
1 => "one",
2 => "two",
_ => "other",
}
}
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 5);
}
#[test]
fn test_collector_impl_methods() {
let source = r#"
struct Foo;
impl Foo {
fn bar(&self) {
let x = 42;
}
const VALUE: i32 = 100;
}
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 2);
assert!(literals.iter().any(|l| l.value == "42"));
assert!(literals.iter().any(|l| l.value == "100"));
}
#[test]
fn test_collector_closure() {
let source = r#"
fn foo() {
let f = |x| x + 1;
}
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 1);
assert_eq!(literals[0].value, "1");
}
#[test]
fn test_collector_array_tuple() {
let source = r#"
fn foo() {
let arr = [1, 2, 3];
let tup = (4, "five", true);
}
"#;
let literals = collect_from_source(source);
assert_eq!(literals.len(), 6);
}
#[test]
fn test_collector_macro_literals() {
let source = r#"
fn foo() {
println!("hello world");
anyhow::anyhow!("Key '{}' not found", key);
format!("error: {}", msg);
}
"#;
let literals = collect_from_source(source);
assert!(literals.iter().any(|l| l.value == "\"hello world\""));
assert!(literals.iter().any(|l| l.value == "\"Key '{}' not found\""));
assert!(literals.iter().any(|l| l.value == "\"error: {}\""));
}
#[test]
fn test_collector_macro_multiple_strings() {
let source = r#"
fn foo() {
write!(f, "name: {}, age: {}", name, age);
}
"#;
let literals = collect_from_source(source);
assert!(literals.iter().any(|l| l.value == "\"name: {}, age: {}\""));
}
#[test]
fn test_collector_doc_comments_on_function() {
let source = r#"
/// This function does something important.
/// It has multiple lines.
fn documented_fn() {}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(!docs.is_empty(), "Should collect doc comments");
assert!(docs.iter().any(|l| l.value.contains("important")));
assert!(docs.iter().any(|l| l.value.contains("multiple lines")));
}
#[test]
fn test_collector_doc_comments_on_struct() {
let source = r#"
/// Configuration for the application.
pub struct Config {
name: String,
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(!docs.is_empty(), "Should collect struct doc comments");
assert!(docs.iter().any(|l| l.value.contains("Configuration")));
}
#[test]
fn test_collector_doc_comments_on_enum() {
let source = r#"
/// Status of the task.
enum Status {
Pending,
Done,
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(!docs.is_empty(), "Should collect enum doc comments");
assert!(docs.iter().any(|l| l.value.contains("Status")));
}
#[test]
fn test_extract_doc_content() {
assert_eq!(extract_doc_content("\" This is a doc\""), "This is a doc");
assert_eq!(
extract_doc_content("\"No leading space\""),
"No leading space"
);
assert_eq!(extract_doc_content("\"\""), "");
assert_eq!(extract_doc_content("bare text"), "bare text");
}
#[test]
fn test_collector_doc_comments_on_impl_method() {
let source = r#"
struct Foo;
impl Foo {
/// Creates a new Foo instance.
fn new() -> Self { Foo }
/// Returns the value.
fn value(&self) -> i32 { 42 }
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter().any(|l| l.value.contains("new Foo instance")),
"Should collect doc comments on impl methods: got {:?}",
docs
);
assert!(
docs.iter().any(|l| l.value.contains("Returns the value")),
"Should collect doc comments on second impl method: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_on_impl_const() {
let source = r#"
struct Foo;
impl Foo {
/// The maximum allowed size.
const MAX_SIZE: usize = 1024;
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter()
.any(|l| l.value.contains("maximum allowed size")),
"Should collect doc comments on impl consts: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_on_impl_block_itself() {
let source = r#"
struct Foo;
/// Implementation of core functionality.
impl Foo {
fn bar() {}
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter().any(|l| l.value.contains("core functionality")),
"Should collect doc comments on impl block itself: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_on_struct_fields() {
let source = r#"
struct Config {
/// The application name.
name: String,
/// The port to listen on.
port: u16,
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter().any(|l| l.value.contains("application name")),
"Should collect doc comments on struct fields: got {:?}",
docs
);
assert!(
docs.iter().any(|l| l.value.contains("port to listen")),
"Should collect doc comments on second struct field: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_on_enum_variants() {
let source = r#"
enum Status {
/// Task is waiting to be processed.
Pending,
/// Task completed successfully.
Done,
/// Task failed with an error.
Failed,
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter()
.any(|l| l.value.contains("waiting to be processed")),
"Should collect doc comments on enum variants: got {:?}",
docs
);
assert!(
docs.iter()
.any(|l| l.value.contains("completed successfully")),
"Should collect doc comments on Done variant: got {:?}",
docs
);
assert!(
docs.iter()
.any(|l| l.value.contains("failed with an error")),
"Should collect doc comments on Failed variant: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_on_trait() {
let source = r#"
/// A trait for processing items.
trait Processor {
fn process(&self);
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter().any(|l| l.value.contains("processing items")),
"Should collect doc comments on trait: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_on_module() {
let source = r#"
/// Module for utilities.
mod utils {
/// A helper function.
fn helper() {}
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.iter().any(|l| l.value.contains("utilities")),
"Should collect doc comments on module: got {:?}",
docs
);
assert!(
docs.iter().any(|l| l.value.contains("helper function")),
"Should collect doc comments on function inside module: got {:?}",
docs
);
}
#[test]
fn test_collector_doc_comments_not_mixed_with_literals() {
let source = r#"
/// Documented function.
fn foo() {
let s = "a string literal";
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
let strings: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::String)
.collect();
assert_eq!(docs.len(), 1);
assert_eq!(strings.len(), 1);
assert!(docs[0].value.contains("Documented function"));
assert_eq!(strings[0].value, "\"a string literal\"");
}
#[test]
fn test_collector_no_doc_comments_when_absent() {
let source = r#"
fn undocumented() {
let x = 42;
}
"#;
let literals = collect_from_source(source);
let docs: Vec<_> = literals
.iter()
.filter(|l| l.kind == LiteralKind::Doc)
.collect();
assert!(
docs.is_empty(),
"Should not collect doc comments when absent"
);
}
fn collect_from_source(source: &str) -> Vec<LiteralInfo> {
use slotmap::KeyData;
let file = PureFile::from_source(source).expect("Failed to parse source");
let file_id = FileId::from(KeyData::from_ffi(1));
let default_symbol = SymbolId::from(KeyData::from_ffi(1));
let mut literals = Vec::new();
LiteralCollector::collect_file(
&file,
file_id,
default_symbol,
|_| Some(default_symbol),
|info| literals.push(info),
);
literals
}
#[test]
fn test_extract_literals_from_fn() {
let source = r#"
fn foo() {
let x = 42;
let s = "hello";
let b = true;
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 3);
assert!(literals.contains(&"42".to_string()));
assert!(literals.contains(&"\"hello\"".to_string()));
assert!(literals.contains(&"true".to_string()));
}
#[test]
fn test_extract_literals_from_const() {
let source = r#"
const MAX: i32 = 100;
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 1);
assert_eq!(literals[0], "100");
}
#[test]
fn test_extract_literals_from_static() {
let source = r#"
static NAME: &str = "test_name";
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 1);
assert_eq!(literals[0], "\"test_name\"");
}
#[test]
fn test_extract_literals_from_impl() {
let source = r#"
impl Foo {
fn bar(&self) {
let x = 42;
}
const VALUE: i32 = 100;
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 2);
assert!(literals.contains(&"42".to_string()));
assert!(literals.contains(&"100".to_string()));
}
#[test]
fn test_extract_literals_nested_expressions() {
let source = r#"
fn foo() {
let x = 1 + 2 * 3;
let y = if true { "a" } else { "b" };
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 6);
assert!(literals.contains(&"1".to_string()));
assert!(literals.contains(&"2".to_string()));
assert!(literals.contains(&"3".to_string()));
assert!(literals.contains(&"true".to_string()));
assert!(literals.contains(&"\"a\"".to_string()));
assert!(literals.contains(&"\"b\"".to_string()));
}
#[test]
fn test_extract_literals_from_match_patterns() {
let source = r#"
fn foo(x: i32) {
match x {
1 => "one",
2 => "two",
_ => "other",
}
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 5);
assert!(literals.contains(&"1".to_string()));
assert!(literals.contains(&"2".to_string()));
assert!(literals.contains(&"\"one\"".to_string()));
assert!(literals.contains(&"\"two\"".to_string()));
assert!(literals.contains(&"\"other\"".to_string()));
}
#[test]
fn test_extract_literals_from_struct_returns_empty() {
let source = r#"
struct Foo {
x: i32,
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert!(literals.is_empty());
}
#[test]
fn test_extract_literals_from_closure() {
let source = r#"
fn foo() {
let f = |x| x + 1;
let g = move || "captured";
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 2);
assert!(literals.contains(&"1".to_string()));
assert!(literals.contains(&"\"captured\"".to_string()));
}
#[test]
fn test_extract_literals_from_array_tuple() {
let source = r#"
fn foo() {
let arr = [1, 2, 3];
let tup = (4, "five");
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert_eq!(literals.len(), 5);
}
#[test]
fn test_extract_literals_from_macro() {
let source = r#"
fn foo() {
println!("hello from macro");
format!("error: {}", msg);
}
"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let item = &file.items[0];
let literals = extract_literals_from_item(item);
assert!(literals.contains(&"\"hello from macro\"".to_string()));
assert!(literals.contains(&"\"error: {}\"".to_string()));
}
#[test]
fn test_extract_string_from_macro_simple() {
let tokens = r#""hello world""#;
let literals = extract_string_literals_from_macro_tokens(tokens);
assert_eq!(literals, vec!["\"hello world\""]);
}
#[test]
fn test_extract_string_from_macro_multiple() {
let tokens = r#""first", var, "second""#;
let literals = extract_string_literals_from_macro_tokens(tokens);
assert_eq!(literals, vec!["\"first\"", "\"second\""]);
}
#[test]
fn test_extract_string_from_macro_escaped_quotes() {
let tokens = r#""hello \"world\"""#;
let literals = extract_string_literals_from_macro_tokens(tokens);
assert_eq!(literals, vec!["\"hello \\\"world\\\"\""]);
}
#[test]
fn test_extract_string_from_macro_format_string() {
let tokens = r#""Key '{}' not found", key"#;
let literals = extract_string_literals_from_macro_tokens(tokens);
assert_eq!(literals, vec!["\"Key '{}' not found\""]);
}
#[test]
fn test_extract_string_from_macro_raw_string() {
let tokens = "r#\"raw string\"#";
let literals = extract_string_literals_from_macro_tokens(tokens);
assert_eq!(literals, vec!["r#\"raw string\"#"]);
}
#[test]
fn test_extract_string_from_macro_raw_string_double_hash() {
let tokens = "r##\"raw with # inside\"##";
let literals = extract_string_literals_from_macro_tokens(tokens);
assert_eq!(literals, vec!["r##\"raw with # inside\"##"]);
}
#[test]
fn test_extract_string_from_macro_no_strings() {
let tokens = "42, foo + bar";
let literals = extract_string_literals_from_macro_tokens(tokens);
assert!(literals.is_empty());
}
#[test]
fn test_extract_string_from_macro_empty() {
let tokens: &str = "";
let literals = extract_string_literals_from_macro_tokens(tokens);
assert!(literals.is_empty());
}
}