use serde::{Deserialize, Serialize};
use crate::ir::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DependencyDomain {
Http,
WebFramework,
Logging,
Testing,
Validation,
Serialization,
Database,
Cli,
AsyncRuntime,
Crypto,
Utilities,
Unknown,
}
impl DependencyDomain {
pub fn as_str(self) -> &'static str {
match self {
Self::Http => "HTTP",
Self::WebFramework => "web framework",
Self::Logging => "logging",
Self::Testing => "testing",
Self::Validation => "validation",
Self::Serialization => "serialization",
Self::Database => "database",
Self::Cli => "CLI",
Self::AsyncRuntime => "async runtime",
Self::Crypto => "crypto",
Self::Utilities => "utilities",
Self::Unknown => "unknown",
}
}
}
pub fn top_level_module(module: &str) -> &str {
let pos = module
.chars()
.position(|c| [' ', ':', '.', '/'].contains(&c));
match pos {
Some(p) => &module[..p],
None => module,
}
}
pub fn is_python_stdlib_module(module: &str) -> bool {
let root = module.split('.').next().unwrap_or(module);
matches!(
root,
"__future__"
| "abc"
| "argparse"
| "ast"
| "asyncio"
| "base64"
| "bisect"
| "builtins"
| "calendar"
| "cmath"
| "codecs"
| "collections"
| "concurrent"
| "configparser"
| "contextlib"
| "copy"
| "csv"
| "ctypes"
| "dataclasses"
| "datetime"
| "decimal"
| "difflib"
| "dis"
| "email"
| "enum"
| "errno"
| "fcntl"
| "fileinput"
| "fnmatch"
| "fractions"
| "functools"
| "gc"
| "getpass"
| "gettext"
| "glob"
| "gzip"
| "hashlib"
| "heapq"
| "hmac"
| "html"
| "http"
| "importlib"
| "inspect"
| "io"
| "ipaddress"
| "itertools"
| "json"
| "keyword"
| "linecache"
| "locale"
| "logging"
| "lzma"
| "math"
| "mimetypes"
| "multiprocessing"
| "numbers"
| "operator"
| "os"
| "pathlib"
| "platform"
| "pprint"
| "queue"
| "random"
| "re"
| "secrets"
| "select"
| "shelve"
| "shlex"
| "shutil"
| "signal"
| "site"
| "socket"
| "sqlite3"
| "ssl"
| "stat"
| "string"
| "struct"
| "subprocess"
| "sys"
| "syslog"
| "tempfile"
| "textwrap"
| "threading"
| "time"
| "timeit"
| "traceback"
| "types"
| "typing"
| "unicodedata"
| "unittest"
| "urllib"
| "uuid"
| "venv"
| "warnings"
| "weakref"
| "xml"
| "zipfile"
| "zipimport"
| "zlib"
)
}
pub fn matches_keyword_at_boundary(name: &str, keywords: &[&str]) -> bool {
let lower = name.to_ascii_lowercase();
let bytes = name.as_bytes();
for kw in keywords {
if kw.is_empty() {
continue;
}
let mut search_start = 0usize;
while let Some(pos) = lower[search_start..].find(kw) {
let abs_pos = search_start + pos;
let prev = abs_pos.checked_sub(1).and_then(|i| bytes.get(i)).copied();
let curr = bytes.get(abs_pos).copied();
let is_boundary = abs_pos == 0
|| prev.is_some_and(|b| b == b'_' || b == b'-')
|| (prev.is_some_and(|b| b.is_ascii_lowercase())
&& curr.is_some_and(|b| b.is_ascii_uppercase()));
if is_boundary {
return true;
}
search_start = abs_pos + 1;
}
}
false
}
pub fn classify_domain(package: &str, language: Language) -> Option<DependencyDomain> {
let normalised = package.to_lowercase().replace('-', "_");
match language {
Language::Rust => classify_rust(&normalised),
Language::TypeScript | Language::JavaScript => classify_js_ts(&normalised),
Language::Python => classify_python(&normalised),
}
}
fn classify_rust(name: &str) -> Option<DependencyDomain> {
match name {
"reqwest" | "hyper" | "ureq" | "curl" | "attohttpc" | "isahc" | "tonic" | "prost"
| "tower" | "tower_http" => Some(DependencyDomain::Http),
"actix_web" | "axum" | "warp" | "rocket" | "tide" | "poem" | "salvo" | "ntex" => {
Some(DependencyDomain::WebFramework)
}
"tracing" | "tracing_subscriber" | "tracing_log" | "log" | "env_logger"
| "pretty_env_logger" | "slog" | "flexi_logger" => Some(DependencyDomain::Logging),
"proptest" | "quickcheck" | "rstest" | "criterion" | "test_case" | "mockall"
| "wiremock" | "assert_cmd" | "assert_fs" | "assert_matches" | "pretty_assertions"
| "insta" | "tempfile" => Some(DependencyDomain::Testing),
"validator" | "garde" | "schemars" => Some(DependencyDomain::Validation),
"serde" | "serde_json" | "serde_yaml" | "serde_toml" | "toml" | "bincode" | "ciborium"
| "postcard" | "rmp_serde" | "ron" | "csv" => Some(DependencyDomain::Serialization),
"sqlx" | "diesel" | "sea_orm" | "rusqlite" | "tokio_postgres" | "deadpool_postgres"
| "mongodb" | "redis" | "surrealdb" => Some(DependencyDomain::Database),
"clap" | "structopt" | "argh" | "pico_args" | "bpaf" => Some(DependencyDomain::Cli),
"tokio" | "async_std" | "smol" | "futures" | "async_trait" | "rayon" | "crossbeam"
| "crossbeam_channel" => Some(DependencyDomain::AsyncRuntime),
"sha2" | "ring" | "rustls" | "openssl" | "aes" | "argon2" | "bcrypt" | "hmac" => {
Some(DependencyDomain::Crypto)
}
"uuid" | "chrono" | "time" | "url" | "bytes" | "indexmap" | "dashmap" | "parking_lot"
| "once_cell" | "lazy_static" | "anyhow" | "thiserror" | "eyre" | "itertools" | "regex"
| "rand" => Some(DependencyDomain::Utilities),
_ => None,
}
}
fn classify_js_ts(name: &str) -> Option<DependencyDomain> {
match name {
"axios"
| "node_fetch"
| "got"
| "ky"
| "superagent"
| "undici"
| "socket_io"
| "socket_io_client"
| "socket.io"
| "socket.io_client"
| "graphql"
| "@apollo/client"
| "urql"
| "@tanstack/react_query"
| "react_query"
| "@tanstack/query_core"
| "swr" => Some(DependencyDomain::Http),
"express" | "fastify" | "koa" | "hapi" | "next" | "hono" | "nest" | "nuxt" | "react"
| "vue" | "angular" | "svelte" | "remix" | "astro" => Some(DependencyDomain::WebFramework),
"winston" | "pino" | "bunyan" | "morgan" | "log4js" | "loglevel" | "debug" | "signale"
| "consola" => Some(DependencyDomain::Logging),
"jest"
| "mocha"
| "vitest"
| "ava"
| "jasmine"
| "chai"
| "sinon"
| "cypress"
| "playwright"
| "testing_library"
| "@testing_library/react"
| "@testing_library/jest_dom"
| "supertest"
| "nock"
| "msw" => Some(DependencyDomain::Testing),
"zod" | "joi" | "yup" | "ajv" | "class_validator" | "superstruct" | "io_ts" | "valibot" => {
Some(DependencyDomain::Validation)
}
"protobufjs" | "avro_js" | "msgpack" | "@msgpack/msgpack" | "flatbuffers" => {
Some(DependencyDomain::Serialization)
}
"prisma" | "@prisma/client" | "typeorm" | "sequelize" | "knex" | "mongoose"
| "drizzle_orm" | "pg" | "mysql2" | "better_sqlite3" | "ioredis" | "redis" => {
Some(DependencyDomain::Database)
}
"commander" | "yargs" | "meow" | "cac" | "citty" | "oclif" | "inquirer" => {
Some(DependencyDomain::Cli)
}
"zustand"
| "redux"
| "@reduxjs/toolkit"
| "recoil"
| "jotai"
| "mobx"
| "xstate"
| "react_router"
| "react_router_dom"
| "@tanstack/react_router"
| "lodash"
| "ramda"
| "underscore"
| "immer"
| "date_fns"
| "dayjs"
| "moment"
| "luxon"
| "dotenv"
| "cross_env"
| "@sentry/react"
| "@sentry/nextjs"
| "@sentry/node" => Some(DependencyDomain::Utilities),
_ => None,
}
}
fn classify_python(name: &str) -> Option<DependencyDomain> {
match name {
"requests" | "httpx" | "aiohttp" | "urllib3" | "httplib2" | "websockets"
| "websocket_client" | "python_socketio" | "grpcio" | "grpcio_tools" => {
Some(DependencyDomain::Http)
}
"flask" | "django" | "fastapi" | "starlette" | "tornado" | "sanic" | "pyramid"
| "bottle" | "litestar" | "blacksheep" => Some(DependencyDomain::WebFramework),
"logging" | "loguru" | "structlog" => Some(DependencyDomain::Logging),
"pytest" | "unittest" | "nose" | "hypothesis" | "mock" | "unittest_mock" | "faker"
| "factory_boy" | "responses" | "pytest_mock" | "pytest_asyncio" | "tox" | "coverage"
| "pytest_cov" => Some(DependencyDomain::Testing),
"pydantic" | "marshmallow" | "cerberus" | "attrs" | "voluptuous" | "cattrs" => {
Some(DependencyDomain::Validation)
}
"json" | "msgpack" | "protobuf" | "avro" | "pickle" | "pyyaml" | "toml" | "orjson"
| "ujson" => Some(DependencyDomain::Serialization),
"sqlalchemy" | "psycopg2" | "asyncpg" | "pymongo" | "redis" | "peewee" | "tortoise"
| "tortoise_orm" | "databases" | "sqlite3" | "alembic" | "aioredis" | "motor" | "neo4j"
| "py2neo" | "pinecone" | "qdrant_client" | "chromadb" | "weaviate_client" | "pymilvus"
| "elasticsearch" | "opensearch_py" => Some(DependencyDomain::Database),
"click" | "argparse" | "typer" | "fire" | "docopt" | "rich" => Some(DependencyDomain::Cli),
"asyncio" | "trio" | "anyio" | "uvloop" | "twisted" | "celery" | "dramatiq" | "uvicorn"
| "gunicorn" | "hypercorn" | "daphne" => Some(DependencyDomain::AsyncRuntime),
"cryptography" | "pycryptodome" | "hashlib" | "passlib" | "bcrypt" | "itsdangerous"
| "jwt" | "python_jose" | "authlib" => Some(DependencyDomain::Crypto),
"openai"
| "anthropic"
| "cohere"
| "google_generativeai"
| "google_genai"
| "langchain"
| "langchain_core"
| "langchain_openai"
| "langchain_anthropic"
| "langfuse"
| "litellm"
| "transformers"
| "sentence_transformers"
| "pandas"
| "numpy"
| "scipy"
| "polars"
| "pyarrow"
| "boto3"
| "botocore"
| "aiobotocore"
| "google_cloud_storage"
| "azure_storage_blob"
| "jinja2"
| "mako"
| "tenacity"
| "backoff"
| "retry"
| "paramiko"
| "fabric"
| "pillow"
| "pil"
| "cv2"
| "opencv_python"
| "stripe"
| "sendgrid"
| "sqlglot"
| "alembic_utils" => Some(DependencyDomain::Utilities),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathAlias {
pub pattern: String,
pub targets: Vec<String>,
}
impl PathAlias {
fn specificity(&self) -> usize {
match self.pattern.split_once('*') {
Some((prefix, _)) => prefix.len(),
None => self.pattern.len() + 1,
}
}
pub fn rewrite(&self, module: &str) -> Option<Vec<String>> {
match self.pattern.split_once('*') {
Some((prefix, suffix)) => {
if module.len() >= prefix.len() + suffix.len()
&& module.starts_with(prefix)
&& module.ends_with(suffix)
{
let captured = &module[prefix.len()..module.len() - suffix.len()];
Some(
self.targets
.iter()
.map(|t| match t.split_once('*') {
Some((tp, ts)) => format!("{tp}{captured}{ts}"),
None => t.clone(),
})
.collect(),
)
} else {
None
}
}
None => (module == self.pattern).then(|| self.targets.clone()),
}
}
}
pub fn resolve_path_alias(module: &str, aliases: &[PathAlias]) -> Vec<String> {
let mut order: Vec<usize> = (0..aliases.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(aliases[i].specificity()));
for i in order {
if let Some(candidates) = aliases[i].rewrite(module) {
return candidates;
}
}
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_http_clients() {
assert_eq!(
classify_domain("reqwest", Language::Rust),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("hyper", Language::Rust),
Some(DependencyDomain::Http)
);
}
#[test]
fn rust_web_frameworks() {
assert_eq!(
classify_domain("axum", Language::Rust),
Some(DependencyDomain::WebFramework)
);
assert_eq!(
classify_domain("actix-web", Language::Rust),
Some(DependencyDomain::WebFramework)
);
}
#[test]
fn rust_logging() {
assert_eq!(
classify_domain("tracing", Language::Rust),
Some(DependencyDomain::Logging)
);
assert_eq!(
classify_domain("log", Language::Rust),
Some(DependencyDomain::Logging)
);
assert_eq!(
classify_domain("tracing-subscriber", Language::Rust),
Some(DependencyDomain::Logging)
);
}
#[test]
fn rust_testing() {
assert_eq!(
classify_domain("proptest", Language::Rust),
Some(DependencyDomain::Testing)
);
assert_eq!(
classify_domain("pretty_assertions", Language::Rust),
Some(DependencyDomain::Testing)
);
assert_eq!(
classify_domain("tempfile", Language::Rust),
Some(DependencyDomain::Testing)
);
}
#[test]
fn rust_serialization() {
assert_eq!(
classify_domain("serde", Language::Rust),
Some(DependencyDomain::Serialization)
);
assert_eq!(
classify_domain("serde-json", Language::Rust),
Some(DependencyDomain::Serialization)
);
assert_eq!(
classify_domain("serde_json", Language::Rust),
Some(DependencyDomain::Serialization)
);
}
#[test]
fn rust_database() {
assert_eq!(
classify_domain("sqlx", Language::Rust),
Some(DependencyDomain::Database)
);
assert_eq!(
classify_domain("sea-orm", Language::Rust),
Some(DependencyDomain::Database)
);
assert_eq!(
classify_domain("rusqlite", Language::Rust),
Some(DependencyDomain::Database)
);
}
#[test]
fn rust_async_runtime() {
assert_eq!(
classify_domain("tokio", Language::Rust),
Some(DependencyDomain::AsyncRuntime)
);
assert_eq!(
classify_domain("async-std", Language::Rust),
Some(DependencyDomain::AsyncRuntime)
);
}
#[test]
fn rust_crypto() {
assert_eq!(
classify_domain("ring", Language::Rust),
Some(DependencyDomain::Crypto)
);
}
#[test]
fn js_ts_http_clients() {
assert_eq!(
classify_domain("axios", Language::TypeScript),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("node-fetch", Language::JavaScript),
Some(DependencyDomain::Http)
);
}
#[test]
fn js_ts_web_frameworks() {
assert_eq!(
classify_domain("express", Language::JavaScript),
Some(DependencyDomain::WebFramework)
);
assert_eq!(
classify_domain("react", Language::TypeScript),
Some(DependencyDomain::WebFramework)
);
assert_eq!(
classify_domain("hono", Language::TypeScript),
Some(DependencyDomain::WebFramework)
);
}
#[test]
fn js_ts_testing() {
assert_eq!(
classify_domain("jest", Language::TypeScript),
Some(DependencyDomain::Testing)
);
assert_eq!(
classify_domain("vitest", Language::TypeScript),
Some(DependencyDomain::Testing)
);
assert_eq!(
classify_domain("cypress", Language::JavaScript),
Some(DependencyDomain::Testing)
);
}
#[test]
fn js_ts_database() {
assert_eq!(
classify_domain("prisma", Language::TypeScript),
Some(DependencyDomain::Database)
);
assert_eq!(
classify_domain("drizzle-orm", Language::TypeScript),
Some(DependencyDomain::Database)
);
}
#[test]
fn python_http_clients() {
assert_eq!(
classify_domain("requests", Language::Python),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("httpx", Language::Python),
Some(DependencyDomain::Http)
);
}
#[test]
fn python_web_frameworks() {
assert_eq!(
classify_domain("django", Language::Python),
Some(DependencyDomain::WebFramework)
);
assert_eq!(
classify_domain("fastapi", Language::Python),
Some(DependencyDomain::WebFramework)
);
}
#[test]
fn python_testing() {
assert_eq!(
classify_domain("pytest", Language::Python),
Some(DependencyDomain::Testing)
);
assert_eq!(
classify_domain("hypothesis", Language::Python),
Some(DependencyDomain::Testing)
);
}
#[test]
fn python_database() {
assert_eq!(
classify_domain("sqlalchemy", Language::Python),
Some(DependencyDomain::Database)
);
assert_eq!(
classify_domain("asyncpg", Language::Python),
Some(DependencyDomain::Database)
);
}
#[test]
fn python_async_runtime() {
assert_eq!(
classify_domain("asyncio", Language::Python),
Some(DependencyDomain::AsyncRuntime)
);
assert_eq!(
classify_domain("trio", Language::Python),
Some(DependencyDomain::AsyncRuntime)
);
}
#[test]
fn python_crypto() {
assert_eq!(
classify_domain("cryptography", Language::Python),
Some(DependencyDomain::Crypto)
);
}
#[test]
fn python_utilities_ai_ml() {
assert_eq!(
classify_domain("openai", Language::Python),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("anthropic", Language::Python),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("langchain", Language::Python),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("pandas", Language::Python),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("numpy", Language::Python),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("boto3", Language::Python),
Some(DependencyDomain::Utilities)
);
}
#[test]
fn python_async_runtime_extended() {
assert_eq!(
classify_domain("celery", Language::Python),
Some(DependencyDomain::AsyncRuntime)
);
assert_eq!(
classify_domain("uvicorn", Language::Python),
Some(DependencyDomain::AsyncRuntime)
);
}
#[test]
fn python_database_extended() {
assert_eq!(
classify_domain("aioredis", Language::Python),
Some(DependencyDomain::Database)
);
assert_eq!(
classify_domain("neo4j", Language::Python),
Some(DependencyDomain::Database)
);
assert_eq!(
classify_domain("qdrant-client", Language::Python),
Some(DependencyDomain::Database)
);
}
#[test]
fn python_http_extended() {
assert_eq!(
classify_domain("websockets", Language::Python),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("grpcio", Language::Python),
Some(DependencyDomain::Http)
);
}
#[test]
fn js_ts_utilities() {
assert_eq!(
classify_domain("zustand", Language::TypeScript),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("redux", Language::TypeScript),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("lodash", Language::JavaScript),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("date-fns", Language::TypeScript),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("dayjs", Language::TypeScript),
Some(DependencyDomain::Utilities)
);
}
#[test]
fn js_ts_http_extended() {
assert_eq!(
classify_domain("socket.io-client", Language::TypeScript),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("swr", Language::TypeScript),
Some(DependencyDomain::Http)
);
}
#[test]
fn rust_utilities() {
assert_eq!(
classify_domain("uuid", Language::Rust),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("chrono", Language::Rust),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("anyhow", Language::Rust),
Some(DependencyDomain::Utilities)
);
assert_eq!(
classify_domain("thiserror", Language::Rust),
Some(DependencyDomain::Utilities)
);
}
#[test]
fn rust_http_extended() {
assert_eq!(
classify_domain("tonic", Language::Rust),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("tower", Language::Rust),
Some(DependencyDomain::Http)
);
}
#[test]
fn unknown_returns_none() {
assert_eq!(classify_domain("my-custom-lib", Language::Rust), None);
assert_eq!(
classify_domain("internal-utils", Language::TypeScript),
None
);
assert_eq!(classify_domain("my_app", Language::Python), None);
}
#[test]
fn hyphen_underscore_normalization() {
assert_eq!(
classify_domain("serde-json", Language::Rust),
classify_domain("serde_json", Language::Rust)
);
assert_eq!(
classify_domain("actix-web", Language::Rust),
classify_domain("actix_web", Language::Rust)
);
assert_eq!(
classify_domain("node-fetch", Language::JavaScript),
classify_domain("node_fetch", Language::JavaScript)
);
}
#[test]
fn case_insensitive() {
assert_eq!(
classify_domain("Reqwest", Language::Rust),
Some(DependencyDomain::Http)
);
assert_eq!(
classify_domain("AXIOS", Language::TypeScript),
Some(DependencyDomain::Http)
);
}
#[test]
fn keyword_boundary_start_of_string() {
assert!(matches_keyword_at_boundary("ormlib", &["orm"]));
assert!(matches_keyword_at_boundary("test_helper", &["test"]));
}
#[test]
fn keyword_boundary_after_separator() {
assert!(matches_keyword_at_boundary("my_orm_lib", &["orm"]));
assert!(matches_keyword_at_boundary("my-orm-lib", &["orm"]));
assert!(matches_keyword_at_boundary("a_test_b", &["test"]));
}
#[test]
fn keyword_boundary_camel_case() {
assert!(matches_keyword_at_boundary("myOrmLib", &["orm"]));
assert!(matches_keyword_at_boundary("notTestLib", &["test"]));
}
#[test]
fn keyword_substring_inside_word_does_not_match() {
assert!(!matches_keyword_at_boundary("format", &["orm"]));
assert!(!matches_keyword_at_boundary("request_id", &["test"]));
assert!(!matches_keyword_at_boundary("timestamp", &["test"]));
assert!(!matches_keyword_at_boundary("inspect", &["spec"]));
}
#[test]
fn keyword_empty_keyword_does_not_loop_or_match() {
assert!(!matches_keyword_at_boundary("anything", &[""]));
assert!(matches_keyword_at_boundary("orm_lib", &["", "orm", ""]));
}
#[test]
fn keyword_empty_keyword_list_returns_false() {
assert!(!matches_keyword_at_boundary("orm_lib", &[]));
}
#[test]
fn keyword_non_ascii_input_degrades_gracefully() {
assert!(!matches_keyword_at_boundary("ормлиб", &["orm"]));
let _ = matches_keyword_at_boundary("İorm_lib", &["orm"]);
}
#[test]
fn keyword_multiple_keywords_first_match_wins() {
assert!(matches_keyword_at_boundary("my_log_pkg", &["http", "log"]));
assert!(matches_keyword_at_boundary("my_log_pkg", &["log", "http"]));
}
fn alias(pattern: &str, targets: &[&str]) -> PathAlias {
PathAlias {
pattern: pattern.to_owned(),
targets: targets.iter().map(|s| (*s).to_owned()).collect(),
}
}
#[test]
fn alias_wildcard_substitutes_capture() {
let a = alias("@app/*", &["src/*"]);
assert_eq!(a.rewrite("@app/utils"), Some(vec!["src/utils".to_owned()]));
assert_eq!(
a.rewrite("@app/foo/bar"),
Some(vec!["src/foo/bar".to_owned()])
);
}
#[test]
fn alias_wildcard_requires_prefix_and_suffix() {
let a = alias("@app/*", &["src/*"]);
assert_eq!(a.rewrite("@other/utils"), None);
assert_eq!(a.rewrite("@app/"), Some(vec!["src/".to_owned()]));
}
#[test]
fn alias_exact_match_only() {
let a = alias("@config", &["src/config/index.ts"]);
assert_eq!(
a.rewrite("@config"),
Some(vec!["src/config/index.ts".to_owned()])
);
assert_eq!(a.rewrite("@config/extra"), None);
}
#[test]
fn alias_multiple_targets_preserve_order() {
let a = alias("@app/*", &["src/*", "generated/*"]);
assert_eq!(
a.rewrite("@app/x"),
Some(vec!["src/x".to_owned(), "generated/x".to_owned()])
);
}
#[test]
fn alias_target_without_wildcard_is_verbatim() {
let a = alias("@app/*", &["src/shim.ts"]);
assert_eq!(
a.rewrite("@app/anything"),
Some(vec!["src/shim.ts".to_owned()])
);
}
#[test]
fn resolve_picks_most_specific_regardless_of_order() {
let aliases = vec![
alias("@app/*", &["src/*"]),
alias("@app/feature/*", &["src/feature/impl/*"]),
];
assert_eq!(
resolve_path_alias("@app/feature/x", &aliases),
vec!["src/feature/impl/x".to_owned()]
);
assert_eq!(
resolve_path_alias("@app/other", &aliases),
vec!["src/other".to_owned()]
);
}
#[test]
fn resolve_no_match_is_empty() {
let aliases = vec![alias("@app/*", &["src/*"])];
assert!(resolve_path_alias("react", &aliases).is_empty());
assert!(resolve_path_alias("@app/x", &[]).is_empty());
}
#[test]
fn path_alias_json_roundtrips() {
let aliases = vec![alias("@app/*", &["src/*"]), alias("@config", &["c.ts"])];
let json = serde_json::to_string(&aliases).unwrap();
let back: Vec<PathAlias> = serde_json::from_str(&json).unwrap();
assert_eq!(aliases, back);
}
}