#[derive(Debug, PartialEq, Eq)]
pub struct MethodParts {
pub type_params: Vec<String>,
pub params: Vec<String>,
pub ret: String,
pub throws: Vec<String>,
}
struct Cursor<'a> {
s: &'a [u8],
i: usize,
}
impl<'a> Cursor<'a> {
const fn new(s: &'a str) -> Self {
Self { s: s.as_bytes(), i: 0 }
}
fn peek(&self) -> Option<u8> {
self.s.get(self.i).copied()
}
fn bump(&mut self) -> Option<u8> {
let c = self.peek()?;
self.i += 1;
Some(c)
}
fn eat(&mut self, c: u8) -> bool {
if self.peek() == Some(c) {
self.i += 1;
true
} else {
false
}
}
const fn at_end(&self) -> bool {
self.i >= self.s.len()
}
fn ident(&mut self, stop: &[u8]) -> Option<String> {
let start = self.i;
while let Some(c) = self.peek() {
if stop.contains(&c) {
break;
}
self.i += 1;
}
let bytes = self.s.get(start..self.i)?;
(!bytes.is_empty()).then(|| String::from_utf8_lossy(bytes).into_owned())
}
}
const fn base_type(c: u8) -> Option<&'static str> {
Some(match c {
b'B' => "byte",
b'C' => "char",
b'D' => "double",
b'F' => "float",
b'I' => "int",
b'J' => "long",
b'S' => "short",
b'Z' => "boolean",
b'V' => "void",
_ => return None,
})
}
#[must_use]
pub fn render_type(sig: &str) -> Option<String> {
let mut c = Cursor::new(sig);
let out = type_sig(&mut c)?;
c.at_end().then_some(out)
}
#[must_use]
pub fn render_method(sig: &str) -> Option<MethodParts> {
let mut c = Cursor::new(sig);
let type_params = if c.peek() == Some(b'<') { type_parameters(&mut c)? } else { Vec::new() };
if !c.eat(b'(') {
return None;
}
let mut params = Vec::new();
while c.peek().is_some_and(|b| b != b')') {
params.push(type_sig(&mut c)?);
}
if !c.eat(b')') {
return None;
}
let ret = type_sig(&mut c)?;
let mut throws = Vec::new();
while c.eat(b'^') {
throws.push(type_sig(&mut c)?);
}
c.at_end().then_some(MethodParts { type_params, params, ret, throws })
}
fn type_sig(c: &mut Cursor) -> Option<String> {
let mut dims = 0usize;
while c.eat(b'[') {
dims += 1;
if dims > 255 {
return None;
}
}
let base = match c.peek()? {
b'L' => class_type(c)?,
b'T' => {
c.bump();
let name = c.ident(b";")?;
if !c.eat(b';') {
return None;
}
name
}
other => {
let name = base_type(other)?;
c.bump();
name.to_string()
}
};
Some(format!("{base}{}", "[]".repeat(dims)))
}
fn class_type(c: &mut Cursor) -> Option<String> {
if !c.eat(b'L') {
return None;
}
let mut out = String::new();
loop {
let name = c.ident(b"<;.")?;
if !out.is_empty() {
out.push('.');
}
out.push_str(&name.replace('/', "."));
if c.peek() == Some(b'<') {
out.push_str(&type_arguments(c)?);
}
if c.eat(b';') {
return Some(out);
}
if !c.eat(b'.') {
return None;
}
}
}
fn type_arguments(c: &mut Cursor) -> Option<String> {
if !c.eat(b'<') {
return None;
}
let mut args: Vec<String> = Vec::new();
loop {
match c.peek()? {
b'>' => break,
b'*' => {
c.bump();
args.push("?".to_string());
}
b'+' => {
c.bump();
args.push(format!("? extends {}", type_sig(c)?));
}
b'-' => {
c.bump();
args.push(format!("? super {}", type_sig(c)?));
}
_ => args.push(type_sig(c)?),
}
}
if !c.eat(b'>') || args.is_empty() {
return None;
}
Some(format!("<{}>", args.join(", ")))
}
fn type_parameters(c: &mut Cursor) -> Option<Vec<String>> {
if !c.eat(b'<') {
return None;
}
let mut params = Vec::new();
let mut bounds: Vec<String> = Vec::new();
while c.peek().is_some_and(|b| b != b'>') {
let name = c.ident(b":")?;
bounds.clear();
if !c.eat(b':') {
return None;
}
if c.peek().is_some_and(|b| b != b':' && b != b'>') {
bounds.push(type_sig(c)?);
}
while c.eat(b':') {
bounds.push(type_sig(c)?);
}
bounds.retain(|b| b != "java.lang.Object");
params.push(if bounds.is_empty() { name } else { format!("{name} extends {}", bounds.join(" & ")) });
}
if !c.eat(b'>') || params.is_empty() {
return None;
}
Some(params)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nested_type_arguments_render_as_java_source() {
assert_eq!(
render_type("Ljava/util/List<Ljava/lang/String;>;").as_deref(),
Some("java.util.List<java.lang.String>")
);
assert_eq!(
render_type("Ljava/util/Map<Ljava/lang/Integer;Ljava/util/List<Lcom/x/Widget;>;>;").as_deref(),
Some("java.util.Map<java.lang.Integer, java.util.List<com.x.Widget>>")
);
assert_eq!(
render_type(
"Ljava/util/Map<Ljava/lang/Integer;Ljava/util/Map<Lcom/x/WSIntegradorEnum;Ljava/util/LinkedList<Lcom/x/WSSessao;>;>;>;"
)
.as_deref(),
Some(
"java.util.Map<java.lang.Integer, java.util.Map<com.x.WSIntegradorEnum, java.util.LinkedList<com.x.WSSessao>>>"
)
);
}
#[test]
fn wildcards_render_the_way_java_writes_them() {
assert_eq!(
render_type("Ljava/util/Map<Ljava/lang/String;+Ljava/lang/Number;>;").as_deref(),
Some("java.util.Map<java.lang.String, ? extends java.lang.Number>")
);
assert_eq!(
render_type("Ljava/util/List<-Ljava/lang/Integer;>;").as_deref(),
Some("java.util.List<? super java.lang.Integer>")
);
assert_eq!(
render_type("Ljava/util/List<*>;").as_deref(),
Some("java.util.List<?>"),
"an unbounded wildcard is the whole argument — there is no type after the `*`"
);
}
#[test]
fn type_variables_and_arrays() {
assert_eq!(render_type("TT;").as_deref(), Some("T"));
assert_eq!(render_type("[TT;").as_deref(), Some("T[]"));
assert_eq!(render_type("[[Ljava/util/List<TE;>;").as_deref(), Some("java.util.List<E>[][]"));
assert_eq!(render_type("[I").as_deref(), Some("int[]"), "a base type still parses here");
}
#[test]
fn a_nested_type_keeps_the_outers_arguments() {
assert_eq!(
render_type("Lcom/x/Outer<Ljava/lang/String;>.Inner<Ljava/lang/Integer;>;").as_deref(),
Some("com.x.Outer<java.lang.String>.Inner<java.lang.Integer>")
);
assert_eq!(
render_type("Lcom/x/Outer.Inner;").as_deref(),
Some("com.x.Outer.Inner"),
"and a nested type with no arguments at all still parses"
);
}
#[test]
fn method_signatures_split_into_the_pieces_a_declaration_needs() {
let plain =
render_method("(Ljava/util/List<Ljava/lang/String;>;I)Ljava/util/Map<Ljava/lang/String;TT;>;")
.expect("a plain generic method signature must parse");
assert!(plain.type_params.is_empty());
assert_eq!(plain.params, vec!["java.util.List<java.lang.String>".to_string(), "int".to_string()]);
assert_eq!(plain.ret, "java.util.Map<java.lang.String, T>");
assert!(plain.throws.is_empty());
let generic = render_method("<T:Ljava/lang/Object;>(TT;)TT;").expect("a generic method must parse");
assert_eq!(
generic.type_params,
vec!["T".to_string()],
"the universal bound is left off, as Java leaves it"
);
assert_eq!(generic.params, vec!["T".to_string()]);
assert_eq!(generic.ret, "T");
let bounded = render_method("<T:Ljava/lang/Number;:Ljava/lang/Comparable<TT;>;>(TT;)V")
.expect("an intersection bound must parse");
assert_eq!(
bounded.type_params,
vec!["T extends java.lang.Number & java.lang.Comparable<T>".to_string()]
);
assert_eq!(bounded.ret, "void");
let thrown = render_method("<E:Ljava/lang/Throwable;>()V^TE;").expect("a generic throws must parse");
assert_eq!(thrown.throws, vec!["E".to_string()]);
let iface_only = render_method("<T::Ljava/lang/Comparable<TT;>;>(TT;)V")
.expect("an interface-only bound (`::`) must parse");
assert_eq!(iface_only.type_params, vec!["T extends java.lang.Comparable<T>".to_string()]);
}
#[test]
fn nothing_unparseable_ever_renders_as_a_type() {
for bad in [
"", "Ljava/util/List<Ljava/lang/String;>", "Ljava/util/List<;>;", "Ljava/util/List<>;", "L;", "T;", "Q", "Ljava/lang/String;X", "[", "*", ] {
assert!(render_type(bad).is_none(), "'{bad}' must not render as a type, but it did");
}
for bad in [
"",
"(I)", "Ljava/lang/String;", "(I)VX", "<>(I)V", "<T>(I)V", "(TT)V", ] {
assert!(render_method(bad).is_none(), "'{bad}' must not render as a method, but it did");
}
}
#[test]
fn an_ungeneric_signature_renders_what_the_plain_one_does() {
for (sig, want) in [
("Ljava/lang/String;", "java.lang.String"),
("[Ljava/lang/String;", "java.lang.String[]"),
("I", "int"),
("[[D", "double[][]"),
("Lcom/x/Order$Line;", "com.x.Order$Line"),
] {
assert_eq!(render_type(sig).as_deref(), Some(want), "{sig}");
}
}
}