use rustdoc_ir::{ScalarPrimitive, Type};
use rustdoc_processor::queries::Crate;
use rustdoc_processor::GlobalItemId;
use crate::Collection;
use crate::diagnostic::DiagnosticSink;
use rustdoc_resolver::{TypeAliasResolution, resolve_type};
use rustdoc_types::{Item, ItemEnum};
pub struct ConstantItem {
pub name: String,
pub value: String,
pub rustdoc_id: GlobalItemId,
}
pub fn resolve_constant(
item: &Item,
krate: &Crate,
collection: &Collection,
diagnostics: &mut DiagnosticSink,
) -> Option<ConstantItem> {
let ItemEnum::Constant { type_, const_ } = &item.inner else {
unreachable!("Expected a constant item");
};
let name = item.name.clone().unwrap_or_else(|| "<unnamed>".to_string());
let resolved = match resolve_type(
type_,
&krate.core.package_id,
collection,
&Default::default(),
TypeAliasResolution::ResolveThrough,
) {
Ok(t) => t,
Err(e) => {
diagnostics
.warning(format!("constant `{name}`: failed to resolve type"))
.with_span_if(item.span.as_ref())
.with_error_chain(&e)
.emit();
return None;
}
};
let value = match &resolved {
Type::ScalarPrimitive(ScalarPrimitive::Bool) => {
let Some(ref value) = const_.value else {
diagnostics
.warning(format!("constant `{name}` has no evaluated value"))
.with_span_if(item.span.as_ref())
.emit();
return None;
};
value.clone()
}
Type::ScalarPrimitive(ScalarPrimitive::Char) => {
if !const_.is_literal {
diagnostics
.warning(format!("constant `{name}` is a computed char expression"))
.with_span_if(item.span.as_ref())
.with_help("only literal char constants are supported")
.emit();
return None;
}
sanitize_char_literal(&const_.expr)
}
Type::ScalarPrimitive(prim)
if !matches!(
prim,
ScalarPrimitive::Str | ScalarPrimitive::U128 | ScalarPrimitive::I128
) =>
{
let Some(ref value) = const_.value else {
diagnostics
.warning(format!("constant `{name}` has no evaluated value"))
.with_span_if(item.span.as_ref())
.emit();
return None;
};
sanitize_rust_number(value, prim)
}
Type::Reference(r) if matches!(&*r.inner, Type::ScalarPrimitive(ScalarPrimitive::Str)) => {
if !const_.is_literal {
diagnostics
.warning(format!("constant `{name}` is a computed string expression"))
.with_span_if(item.span.as_ref())
.with_help("only literal string constants are supported")
.emit();
return None;
}
const_.expr.clone()
}
_ => {
diagnostics
.warning(format!("constant `{name}` has unsupported type"))
.with_span_if(item.span.as_ref())
.emit();
return None;
}
};
Some(ConstantItem {
name,
value,
rustdoc_id: GlobalItemId::new(item.id, krate.core.package_id.clone()),
})
}
pub fn resolve_assoc_constant(
item: &Item,
type_name: &str,
krate: &Crate,
collection: &Collection,
diagnostics: &mut DiagnosticSink,
) -> Option<ConstantItem> {
let ItemEnum::AssocConst { type_, value } = &item.inner else {
unreachable!("Expected an AssocConst item");
};
let const_name = item.name.clone().unwrap_or_else(|| "<unnamed>".to_string());
let resolved = match resolve_type(
type_,
&krate.core.package_id,
collection,
&Default::default(),
TypeAliasResolution::ResolveThrough,
) {
Ok(t) => t,
Err(e) => {
diagnostics
.warning(format!(
"assoc constant `{type_name}_{const_name}`: failed to resolve type"
))
.with_span_if(item.span.as_ref())
.with_error_chain(&e)
.emit();
return None;
}
};
let raw_value = match value {
Some(v) if !v.is_empty() && v != "_" => v,
_ => {
diagnostics
.warning(format!(
"assoc constant `{type_name}_{const_name}` has no evaluated value"
))
.with_span_if(item.span.as_ref())
.emit();
return None;
}
};
let value = match &resolved {
Type::ScalarPrimitive(ScalarPrimitive::Bool) => raw_value.clone(),
Type::ScalarPrimitive(ScalarPrimitive::Char) => sanitize_char_literal(raw_value),
Type::ScalarPrimitive(prim)
if !matches!(
prim,
ScalarPrimitive::Str | ScalarPrimitive::U128 | ScalarPrimitive::I128
) =>
{
sanitize_rust_number(raw_value, prim)
}
Type::Reference(r) if matches!(&*r.inner, Type::ScalarPrimitive(ScalarPrimitive::Str)) => {
raw_value.clone()
}
_ => {
diagnostics
.warning(format!(
"assoc constant `{type_name}_{const_name}` has unsupported type"
))
.with_span_if(item.span.as_ref())
.emit();
return None;
}
};
Some(ConstantItem {
name: format!("{type_name}_{const_name}"),
value,
rustdoc_id: GlobalItemId::new(item.id, krate.core.package_id.clone()),
})
}
fn sanitize_char_literal(expr: &str) -> String {
let inner = &expr[1..expr.len() - 1];
if inner.starts_with('\\') {
expr.to_string()
} else {
let ch = inner.chars().next().expect("empty char literal");
if ch.is_ascii() {
expr.to_string()
} else {
format!("U'\\U{:08X}'", ch as u32)
}
}
}
fn sanitize_rust_number(value: &str, prim: &ScalarPrimitive) -> String {
let suffix = match prim {
ScalarPrimitive::U8 => "u8",
ScalarPrimitive::U16 => "u16",
ScalarPrimitive::U32 => "u32",
ScalarPrimitive::U64 => "u64",
ScalarPrimitive::Usize => "usize",
ScalarPrimitive::I8 => "i8",
ScalarPrimitive::I16 => "i16",
ScalarPrimitive::I32 => "i32",
ScalarPrimitive::I64 => "i64",
ScalarPrimitive::Isize => "isize",
ScalarPrimitive::F32 => "f32",
ScalarPrimitive::F64 => "f64",
ScalarPrimitive::Bool
| ScalarPrimitive::Char
| ScalarPrimitive::Str
| ScalarPrimitive::U128
| ScalarPrimitive::I128 => {
unreachable!(
"Bool, Char, Str, U128 and I128 are handled by earlier match arms in resolve_constant"
)
}
};
value
.strip_suffix(suffix)
.unwrap_or(value)
.replace('_', "")
}