use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub const SYM_PREFIX: &str = "sym:";
const EXTERN: &str = "extern";
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SymbolId(Arc<str>);
#[derive(Debug, Clone, PartialEq, Eq)]
enum Repr {
Global { package: Package, descriptors: Vec<Descriptor> },
Path(Vec<String>),
Unresolved(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Package {
manager: String,
name: String,
version: Version,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Version {
Current,
Release(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Descriptor {
name: String,
suffix: Suffix,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Suffix {
Namespace,
Type,
Term,
Method {
disambiguator: Option<String>,
},
TypeParameter,
Parameter,
Meta,
Macro,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IdError {
#[error("symbol id must start with `sym:`")]
MissingPrefix,
#[error("invalid package manager `{0}`")]
Manager(String),
#[error("invalid package field `{0}`")]
Field(String),
#[error("invalid method disambiguator `{0}`")]
Disambiguator(String),
#[error("a path id needs at least one segment")]
EmptyPath,
#[error("malformed symbol id at byte {at}: {why}")]
Syntax {
at: usize,
why: &'static str,
},
}
impl Package {
pub fn new(manager: impl Into<String>, name: impl Into<String>, version: Version) -> Result<Self, IdError> {
let manager = manager.into();
let name = name.into();
check_manager(&manager)?;
check_field(&name)?;
if name == "." {
return Err(IdError::Field(name));
}
if let Version::Release(v) = &version {
check_field(v)?;
if v == "." {
return Err(IdError::Field(v.clone()));
}
}
Ok(Self { manager, name, version })
}
pub fn current(manager: impl Into<String>, name: impl Into<String>) -> Result<Self, IdError> {
Self::new(manager, name, Version::Current)
}
pub fn manager(&self) -> &str {
&self.manager
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &Version {
&self.version
}
}
fn check_manager(m: &str) -> Result<(), IdError> {
let mut cs = m.chars();
let ok = matches!(cs.next(), Some('a'..='z'))
&& cs.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& m != EXTERN;
if ok { Ok(()) } else { Err(IdError::Manager(m.to_owned())) }
}
fn check_field(f: &str) -> Result<(), IdError> {
let ok = !f.is_empty() && !f.starts_with(' ') && !f.ends_with(' ') && !f.chars().any(char::is_control);
if ok { Ok(()) } else { Err(IdError::Field(f.to_owned())) }
}
impl Descriptor {
pub fn new(name: impl Into<String>, suffix: Suffix) -> Result<Self, IdError> {
if let Suffix::Method { disambiguator: Some(d) } = &suffix {
if d.is_empty() || !d.chars().all(is_simple) {
return Err(IdError::Disambiguator(d.clone()));
}
}
Ok(Self { name: name.into(), suffix })
}
pub fn namespace(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Namespace }
}
pub fn r#type(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Type }
}
pub fn term(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Term }
}
pub fn method(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Method { disambiguator: None } }
}
pub fn type_parameter(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::TypeParameter }
}
pub fn parameter(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Parameter }
}
pub fn meta(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Meta }
}
pub fn r#macro(name: impl Into<String>) -> Self {
Self { name: name.into(), suffix: Suffix::Macro }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn suffix(&self) -> &Suffix {
&self.suffix
}
fn write(&self, out: &mut String) {
match &self.suffix {
Suffix::TypeParameter => {
out.push('[');
write_name(&self.name, out);
out.push(']');
}
Suffix::Parameter => {
out.push('(');
write_name(&self.name, out);
out.push(')');
}
other => {
write_name(&self.name, out);
match other {
Suffix::Namespace => out.push('/'),
Suffix::Type => out.push('#'),
Suffix::Term => out.push('.'),
Suffix::Meta => out.push(':'),
Suffix::Macro => out.push('!'),
Suffix::Method { disambiguator } => {
out.push('(');
out.push_str(disambiguator.as_deref().unwrap_or(""));
out.push_str(").");
}
Suffix::TypeParameter | Suffix::Parameter => unreachable!("handled above"),
}
}
}
}
}
fn is_simple(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '_' | '+' | '-' | '$')
}
fn write_name(name: &str, out: &mut String) {
if !name.is_empty() && name.chars().all(is_simple) {
out.push_str(name);
return;
}
out.push('`');
for c in name.chars() {
if c == '`' {
out.push('`');
}
out.push(c);
}
out.push('`');
}
fn write_field(field: &str, out: &mut String) {
for c in field.chars() {
if c == ' ' {
out.push(' ');
}
out.push(c);
}
}
impl SymbolId {
fn from_repr(repr: &Repr) -> Self {
Self(print(repr).into())
}
pub fn global(package: Package, descriptors: Vec<Descriptor>) -> Self {
Self::from_repr(&Repr::Global { package, descriptors })
}
pub fn package_root(package: Package) -> Self {
Self::global(package, Vec::new())
}
pub fn path<I, S>(segments: I) -> Result<Self, IdError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let segs: Vec<String> = segments.into_iter().map(Into::into).collect();
if segs.is_empty() {
return Err(IdError::EmptyPath);
}
Ok(Self::from_repr(&Repr::Path(segs)))
}
pub fn unresolved(name: impl Into<String>) -> Self {
Self::from_repr(&Repr::Unresolved(name.into()))
}
pub fn parse(text: &str) -> Result<Self, IdError> {
let repr = Parser { s: text, pos: 0 }.id()?;
if print(&repr) != text {
return Err(IdError::Syntax { at: 0, why: "not in canonical form" });
}
Ok(Self(text.into()))
}
pub(crate) fn min_value() -> Self {
Self("".into())
}
pub fn as_str(&self) -> &str {
&self.0
}
fn repr(&self) -> Repr {
Parser { s: &self.0, pos: 0 }.id().expect("a SymbolId always holds canonical text")
}
fn shape(&self) -> Shape<'_> {
Shape::of(&self.0)
}
pub fn is_global(&self) -> bool {
matches!(self.shape(), Shape::Global { .. })
}
pub fn is_unresolved(&self) -> bool {
matches!(self.shape(), Shape::Unresolved(_))
}
pub fn package(&self) -> Option<Package> {
match self.repr() {
Repr::Global { package, .. } => Some(package),
_ => None,
}
}
pub fn descriptors(&self) -> Vec<Descriptor> {
match self.repr() {
Repr::Global { descriptors, .. } => descriptors,
_ => Vec::new(),
}
}
pub fn segments(&self) -> Option<Vec<String>> {
match self.repr() {
Repr::Path(s) => Some(s),
_ => None,
}
}
pub fn last(&self) -> Option<Descriptor> {
self.descriptors().pop()
}
pub fn name(&self) -> Cow<'_, str> {
match self.shape() {
Shape::Global { name, descriptors, .. } => {
scan_descriptors(descriptors).last().map_or_else(|| unfield(name), |d| unquote(d.name))
}
Shape::Path(body) => scan_path(body).last().map_or(Cow::Borrowed(""), |s| unquote(s)),
Shape::Unresolved(raw) => unquote(raw),
}
}
pub fn root(&self) -> Option<Cow<'_, str>> {
match self.shape() {
Shape::Global { name, .. } => Some(unfield(name)),
Shape::Path(body) => scan_path(body).first().map(|s| unquote(s)),
Shape::Unresolved(_) => None,
}
}
pub fn parent(&self) -> Option<SymbolId> {
let text: &str = &self.0;
match self.shape() {
Shape::Global { descriptors, .. } => {
let mut spans = scan_descriptors(descriptors);
spans.pop()?;
while spans.last().is_some_and(|d| d.kind == b'[') {
spans.pop();
}
let base = text.len() - descriptors.len();
let end = match spans.last() {
Some(d) => base + d.end,
None => base - 1,
};
Some(Self(text[..end].into()))
}
Shape::Path(body) => {
let segs = scan_path(body);
if segs.len() < 2 {
return None;
}
let last = segs[segs.len() - 1];
let end = text.len() - last.len() - "::".len();
Some(Self(text[..end].into()))
}
Shape::Unresolved(_) => None,
}
}
pub fn child(&self, descriptor: Descriptor) -> Option<SymbolId> {
let Shape::Global { descriptors, .. } = self.shape() else { return None };
let mut out = String::with_capacity(self.0.len() + descriptor.name.len() + 4);
out.push_str(&self.0);
if descriptors.is_empty() {
out.push(' ');
}
descriptor.write(&mut out);
Some(Self(out.into()))
}
pub fn names(&self) -> Vec<Cow<'_, str>> {
match self.shape() {
Shape::Global { name, descriptors, .. } => std::iter::once(unfield(name))
.chain(scan_descriptors(descriptors).into_iter().map(|d| unquote(d.name)))
.collect(),
Shape::Path(body) => scan_path(body).into_iter().map(unquote).collect(),
Shape::Unresolved(raw) => vec![unquote(raw)],
}
}
pub fn extend_path(&self, segment: &str) -> SymbolId {
let mut s: Vec<String> = self.names().into_iter().map(Cow::into_owned).collect();
s.push(segment.to_owned());
Self::from_repr(&Repr::Path(s))
}
pub fn view(&self) -> IdView<'_> {
match self.shape() {
Shape::Global { manager, name, version, descriptors } => IdView::Global(GlobalView {
manager,
package: unfield(name),
version: (version != ".").then(|| unfield(version)),
descriptors: scan_descriptors(descriptors)
.into_iter()
.map(|d| DescriptorView {
name: unquote(d.name),
kind: match d.kind {
b'/' => DescriptorKind::Namespace,
b'#' => DescriptorKind::Type,
b'(' => DescriptorKind::Method,
b'[' => DescriptorKind::TypeParameter,
b')' => DescriptorKind::Parameter,
b':' => DescriptorKind::Meta,
b'!' => DescriptorKind::Macro,
_ => DescriptorKind::Term,
},
disambiguator: (!d.disambiguator.is_empty()).then_some(d.disambiguator),
})
.collect(),
}),
Shape::Path(body) => IdView::Path(scan_path(body).into_iter().map(unquote).collect()),
Shape::Unresolved(raw) => IdView::Unresolved(unquote(raw)),
}
}
pub fn display_path(&self) -> String {
self.names().join("::")
}
}
impl fmt::Debug for SymbolId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SymbolId({:?})", &*self.0)
}
}
impl fmt::Display for SymbolId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for SymbolId {
type Err = IdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Serialize for SymbolId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for SymbolId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
fn print(repr: &Repr) -> String {
let mut out = String::from(SYM_PREFIX);
match repr {
Repr::Global { package, descriptors } => {
out.push_str(&package.manager);
out.push(' ');
write_field(&package.name, &mut out);
out.push(' ');
match &package.version {
Version::Current => out.push('.'),
Version::Release(v) => write_field(v, &mut out),
}
if !descriptors.is_empty() {
out.push(' ');
for d in descriptors {
d.write(&mut out);
}
}
}
Repr::Path(segs) => {
out.push_str(EXTERN);
out.push(' ');
for (i, s) in segs.iter().enumerate() {
if i > 0 {
out.push_str("::");
}
write_name(s, &mut out);
}
}
Repr::Unresolved(n) => {
out.push_str("? ");
write_name(n, &mut out);
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdView<'a> {
Global(GlobalView<'a>),
Path(Vec<Cow<'a, str>>),
Unresolved(Cow<'a, str>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobalView<'a> {
pub manager: &'a str,
pub package: Cow<'a, str>,
pub version: Option<Cow<'a, str>>,
pub descriptors: Vec<DescriptorView<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescriptorView<'a> {
pub name: Cow<'a, str>,
pub kind: DescriptorKind,
pub disambiguator: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DescriptorKind {
Namespace,
Type,
Term,
Method,
TypeParameter,
Parameter,
Meta,
Macro,
}
enum Shape<'a> {
Global { manager: &'a str, name: &'a str, version: &'a str, descriptors: &'a str },
Path(&'a str),
Unresolved(&'a str),
}
impl<'a> Shape<'a> {
fn of(text: &'a str) -> Self {
let body = text.strip_prefix(SYM_PREFIX).unwrap_or(text);
if let Some(raw) = body.strip_prefix("? ") {
return Shape::Unresolved(raw);
}
if let Some(raw) = body.strip_prefix("extern ") {
return Shape::Path(raw);
}
let bytes = body.as_bytes();
let mut seps = Vec::with_capacity(3);
let mut i = 0;
while i < bytes.len() && seps.len() < 3 {
if bytes[i] == b' ' {
let run = bytes[i..].iter().take_while(|&&b| b == b' ').count();
if run % 2 == 1 {
seps.push(i + run - 1);
}
i += run;
} else {
i += 1;
}
}
let (a, b) = match (seps.first(), seps.get(1)) {
(Some(&a), Some(&b)) => (a, b),
_ => (0, 0),
};
let manager = &body[..a];
let name = body.get(a + 1..b).unwrap_or("");
let version = seps.get(2).map_or(body.get(b + 1..).unwrap_or(""), |&c| &body[b + 1..c]);
let descriptors = seps.get(2).map_or("", |&c| &body[c + 1..]);
Shape::Global { manager, name, version, descriptors }
}
}
struct DescSpan<'a> {
end: usize,
name: &'a str,
kind: u8,
disambiguator: &'a str,
}
fn raw_name_len(s: &str) -> usize {
let b = s.as_bytes();
if b.first() == Some(&b'`') {
let mut i = 1;
while i < b.len() {
if b[i] == b'`' {
if b.get(i + 1) == Some(&b'`') {
i += 2;
continue;
}
return i + 1;
}
i += 1;
}
return b.len();
}
s.char_indices().find(|&(_, c)| !is_simple(c)).map_or(s.len(), |(i, _)| i)
}
fn scan_descriptors(s: &str) -> Vec<DescSpan<'_>> {
let mut out = Vec::new();
let mut pos = 0;
while pos < s.len() {
let rest = &s[pos..];
let (kind, name, len, disambiguator) = match rest.as_bytes()[0] {
open @ (b'[' | b'(') => {
let n = raw_name_len(&rest[1..]);
(if open == b'(' { b')' } else { b'[' }, &rest[1..1 + n], n + 2, "")
}
_ => {
let n = raw_name_len(rest);
let kind = rest.as_bytes().get(n).copied().unwrap_or(b'.');
if kind == b'(' {
let close = rest[n..].find(").").unwrap_or(rest.len() - n);
(kind, &rest[..n], n + close + 2, rest.get(n + 1..n + close).unwrap_or(""))
} else {
(kind, &rest[..n], n + 1, "")
}
}
};
pos += len.max(1);
out.push(DescSpan { end: pos.min(s.len()), name, kind, disambiguator });
}
out
}
fn scan_path(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut pos = 0;
loop {
let n = raw_name_len(&s[pos..]);
out.push(&s[pos..pos + n]);
pos += n;
if s[pos..].starts_with("::") {
pos += 2;
} else {
return out;
}
}
}
fn unquote(raw: &str) -> Cow<'_, str> {
match raw.strip_prefix('`').and_then(|r| r.strip_suffix('`')) {
Some(inner) if inner.contains("``") => Cow::Owned(inner.replace("``", "`")),
Some(inner) => Cow::Borrowed(inner),
None => Cow::Borrowed(raw),
}
}
fn unfield(raw: &str) -> Cow<'_, str> {
if raw.contains(" ") { Cow::Owned(raw.replace(" ", " ")) } else { Cow::Borrowed(raw) }
}
struct Parser<'a> {
s: &'a str,
pos: usize,
}
impl Parser<'_> {
fn err(&self, why: &'static str) -> IdError {
IdError::Syntax { at: self.pos, why }
}
fn rest(&self) -> &str {
&self.s[self.pos..]
}
fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
fn eat(&mut self, lit: &str) -> bool {
if self.rest().starts_with(lit) {
self.pos += lit.len();
true
} else {
false
}
}
fn expect(&mut self, lit: &str, why: &'static str) -> Result<(), IdError> {
if self.eat(lit) { Ok(()) } else { Err(self.err(why)) }
}
fn done(&self) -> Result<(), IdError> {
if self.pos == self.s.len() { Ok(()) } else { Err(self.err("unexpected trailing text")) }
}
fn id(mut self) -> Result<Repr, IdError> {
if !self.eat(SYM_PREFIX) {
return Err(IdError::MissingPrefix);
}
if self.eat("? ") {
let name = self.name()?;
self.done()?;
return Ok(Repr::Unresolved(name));
}
if self.eat("extern ") {
let mut segs = vec![self.name()?];
while self.eat("::") {
segs.push(self.name()?);
}
self.done()?;
return Ok(Repr::Path(segs));
}
let manager = self.field()?;
self.expect(" ", "expected a space after the manager")?;
let name = self.field()?;
self.expect(" ", "expected a space after the package name")?;
let version = self.field()?;
let version = if version == "." { Version::Current } else { Version::Release(version) };
let package = Package::new(manager, name, version)?;
let mut descriptors = Vec::new();
if self.pos < self.s.len() {
self.expect(" ", "expected a space before the descriptors")?;
if self.pos == self.s.len() {
return Err(self.err("expected a descriptor"));
}
while self.pos < self.s.len() {
descriptors.push(self.descriptor()?);
}
}
Ok(Repr::Global { package, descriptors })
}
fn field(&mut self) -> Result<String, IdError> {
let mut out = String::new();
loop {
let rest = self.rest();
if rest.starts_with(" ") {
out.push(' ');
self.pos += 2;
continue;
}
match rest.chars().next() {
None | Some(' ') => break,
Some(c) => {
out.push(c);
self.pos += c.len_utf8();
}
}
}
if out.is_empty() {
return Err(self.err("empty package field"));
}
Ok(out)
}
fn name(&mut self) -> Result<String, IdError> {
if self.eat("`") {
let mut out = String::new();
loop {
match self.peek() {
None => return Err(self.err("unterminated quoted name")),
Some('`') => {
if self.rest().starts_with("``") {
out.push('`');
self.pos += 2;
} else {
if !out.is_empty() && out.chars().all(is_simple) {
return Err(self.err("a simple name must not be quoted"));
}
self.pos += 1;
return Ok(out);
}
}
Some(c) => {
out.push(c);
self.pos += c.len_utf8();
}
}
}
}
let start = self.pos;
while self.peek().is_some_and(is_simple) {
self.pos += 1;
}
if self.pos == start {
return Err(self.err("expected a name"));
}
Ok(self.s[start..self.pos].to_owned())
}
fn descriptor(&mut self) -> Result<Descriptor, IdError> {
if self.eat("[") {
let name = self.name()?;
self.expect("]", "expected `]`")?;
return Ok(Descriptor::type_parameter(name));
}
if self.eat("(") {
let name = self.name()?;
self.expect(")", "expected `)`")?;
return Ok(Descriptor::parameter(name));
}
let name = self.name()?;
let suffix = match self.peek() {
Some('/') => Suffix::Namespace,
Some('#') => Suffix::Type,
Some('.') => Suffix::Term,
Some(':') => Suffix::Meta,
Some('!') => Suffix::Macro,
Some('(') => {
self.pos += 1;
let start = self.pos;
while self.peek().is_some_and(is_simple) {
self.pos += 1;
}
let d = &self.s[start..self.pos];
let disambiguator = (!d.is_empty()).then(|| d.to_owned());
self.expect(").", "expected `).`")?;
return Descriptor::new(name, Suffix::Method { disambiguator });
}
_ => return Err(self.err("expected a descriptor suffix")),
};
self.pos += 1;
Ok(Descriptor { name, suffix })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rt(s: &str) {
let id = SymbolId::parse(s).unwrap_or_else(|e| panic!("{s}: {e}"));
assert_eq!(id.to_string(), s);
}
#[test]
fn round_trips_every_form() {
rt("sym:cargo a .");
rt("sym:cargo a . b/");
rt("sym:cargo a . b/C#d().");
rt("sym:cargo a 1.2.0 b/C#d(+1).");
rt("sym:cargo a . T#[`From<a::B>`]from().");
rt("sym:npm @x/y . index/`POST /api`().");
rt("sym:npm a b . c/");
rt("sym:cargo a . f().(x)[T]m:k!v.");
rt("sym:cargo a . ``#```a``b```.");
rt("sym:extern tokio::spawn");
rt("sym:extern `?`::`a b`");
rt("sym:? insert");
rt("sym:? ``");
}
#[test]
fn rejects_malformed() {
for bad in [
"",
"cargo a .",
"sym:",
"sym:cargo",
"sym:cargo a",
"sym:cargo a . ",
"sym:Cargo a .",
"sym:extern",
"sym:extern a::",
"sym:cargo a . b",
"sym:cargo a . `b",
"sym:cargo a . b(x y).",
"sym:cargo a . b().c",
"sym:cargo . .",
"sym:cargo a .",
"sym:? a b",
"sym:? `a`",
] {
assert!(SymbolId::parse(bad).is_err(), "accepted {bad:?}");
}
}
#[test]
fn json_is_the_canonical_string() {
let id = SymbolId::parse("sym:cargo a . b/C#").unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"sym:cargo a . b/C#\"");
assert_eq!(serde_json::from_str::<SymbolId>(&json).unwrap(), id);
assert!(serde_json::from_str::<SymbolId>("\"a::b\"").is_err());
}
}