use anyhow::{bail, Result};
use super::manifest::DependencyKind;
const LEADING_DIGIT_PREFIX: &str = "pkg-";
const RESERVED_SUFFIX: &str = "-pkg";
const MAX_ALIAS_LENGTH: usize = 64;
const RESERVED_IDENTIFIERS: &[&str] = &[
"as",
"async",
"await",
"break",
"const",
"continue",
"crate",
"dyn",
"else",
"enum",
"extern",
"false",
"fn",
"for",
"if",
"impl",
"in",
"let",
"loop",
"match",
"mod",
"move",
"mut",
"pub",
"ref",
"return",
"self",
"static",
"struct",
"super",
"trait",
"true",
"type",
"unsafe",
"use",
"where",
"while",
"abstract",
"become",
"box",
"do",
"final",
"macro",
"override",
"priv",
"try",
"typeof",
"unsized",
"virtual",
"yield",
"gen",
"union",
"core",
"std",
"alloc",
"proc_macro",
"test",
"main",
"lib",
"case",
"catch",
"class",
"debugger",
"default",
"delete",
"export",
"extends",
"finally",
"function",
"import",
"instanceof",
"new",
"null",
"switch",
"this",
"throw",
"var",
"void",
"with",
"implements",
"interface",
"package",
"private",
"protected",
"public",
"arguments",
"eval",
"undefined",
"any",
"boolean",
"number",
"string",
"symbol",
"never",
"unknown",
"object",
"bigint",
"declare",
"namespace",
"module",
"require",
"readonly",
"keyof",
"infer",
"and",
"assert",
"def",
"del",
"elif",
"except",
"from",
"global",
"is",
"lambda",
"nonlocal",
"not",
"or",
"pass",
"raise",
"none",
"typing",
"dataclasses",
"asyncio",
"json",
"enum_",
"types",
"builtins",
"sys",
"os",
];
pub fn derive_local_alias(lookup: &str) -> String {
let mut normalized = String::with_capacity(lookup.len());
let mut pending_separator = false;
for character in lookup.chars() {
if character.is_ascii_alphanumeric() {
if pending_separator && !normalized.is_empty() {
normalized.push('-');
}
pending_separator = false;
normalized.push(character.to_ascii_lowercase());
} else {
pending_separator = true;
}
}
if normalized.is_empty() {
normalized.push_str("dependency");
}
if normalized
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_digit())
{
normalized.insert_str(0, LEADING_DIGIT_PREFIX);
}
if is_reserved_identifier(&normalized) {
normalized.push_str(RESERVED_SUFFIX);
}
if normalized.len() > MAX_ALIAS_LENGTH {
normalized.truncate(MAX_ALIAS_LENGTH);
while normalized.ends_with('-') {
normalized.pop();
}
}
normalized
}
pub fn is_reserved_identifier(alias: &str) -> bool {
let identifier = alias.replace('-', "_");
RESERVED_IDENTIFIERS
.iter()
.any(|reserved| reserved.eq_ignore_ascii_case(&identifier))
}
pub fn validate_local_alias(alias: &str, kind: DependencyKind) -> Result<()> {
let valid_shape = !alias.is_empty()
&& alias.len() <= MAX_ALIAS_LENGTH
&& alias
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_lowercase())
&& alias.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')
})
&& !alias.ends_with(['-', '_'])
&& !alias.contains("--")
&& !alias.contains("__");
if !valid_shape {
bail!(
"{kind} alias '{alias}' must be 1-64 lowercase ASCII letters, digits, '-' or '_', start with a letter, and not end with or repeat separators (try the derived alias '{}')",
derive_local_alias(alias)
);
}
if is_reserved_identifier(alias) {
bail!(
"{kind} alias '{alias}' is a reserved identifier in a generated language (try '{}')",
derive_local_alias(alias)
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aliases_are_deterministic_and_portable() {
let cases: &[(&str, &str)] = &[
("ore", "ore"),
("My-Program", "my-program"),
("Plan004_Shared", "plan004-shared"),
("upr_AbC123-xyz_9", "upr-abc123-xyz-9"),
("UPR_ONLYCAPS", "upr-onlycaps"),
("demo.stack/one", "demo-stack-one"),
(" spaced name ", "spaced-name"),
("1inch", "pkg-1inch"),
("42", "pkg-42"),
("---", "dependency"),
("", "dependency"),
("émoji ✨ stack", "moji-stack"),
("default", "default-pkg"),
("Class", "class-pkg"),
("import", "import-pkg"),
("proc-macro", "proc-macro-pkg"),
("None", "none-pkg"),
("self", "self-pkg"),
("a--b__c", "a-b-c"),
];
for (lookup, expected) in cases {
let alias = derive_local_alias(lookup);
assert_eq!(&alias, expected, "lookup {lookup:?}");
assert_eq!(
derive_local_alias(&alias),
alias,
"derivation is idempotent for {lookup:?}"
);
validate_local_alias(&alias, DependencyKind::Program)
.unwrap_or_else(|error| panic!("derived alias {alias:?} must validate: {error}"));
}
}
#[test]
fn colliding_lookups_normalize_identically_and_are_detectable() {
assert_eq!(
derive_local_alias("My_Program"),
derive_local_alias("my-program")
);
assert_eq!(
derive_local_alias("MY.PROGRAM"),
derive_local_alias("my program")
);
}
#[test]
fn long_lookups_are_bounded_without_trailing_separator() {
let long = format!("{}-tail", "a".repeat(70));
let alias = derive_local_alias(&long);
assert_eq!(alias.len(), 64);
assert!(!alias.ends_with('-'));
validate_local_alias(&alias, DependencyKind::Stack).unwrap();
}
#[test]
fn explicit_aliases_must_be_valid_identifiers_in_every_language() {
for alias in ["ore", "my-program", "upr-abc", "a1", "snake_case"] {
validate_local_alias(alias, DependencyKind::Program).unwrap();
}
for alias in [
"Default",
"1abc",
"class",
"trailing-",
"double--dash",
"",
"UPR_X",
"fn",
"self",
"import",
] {
assert!(
validate_local_alias(alias, DependencyKind::Stack).is_err(),
"{alias:?} must be rejected"
);
}
}
}