use std::{
error::Error,
fmt::{self, Display},
str::FromStr,
};
pub use ruff_python_stdlib::identifiers::is_identifier;
use serde::{Deserialize, Serialize};
use strum::VariantNames;
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
strum::VariantNames,
)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum TypeCheckingFormat {
#[default]
Full,
Concise,
Azure,
Json,
#[strum(to_string = "jsonlines", serialize = "json-lines")]
JsonLines,
Rdjson,
Pylint,
Gitlab,
Github,
}
impl TypeCheckingFormat {
pub fn from_name(name: &str) -> Result<Self, String> {
Self::from_str(name)
.map_err(|_| format!("unknown type check format '{name}', expected one of: {}", Self::names()))
}
#[must_use]
pub fn names() -> String {
Self::VARIANTS.join(", ")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeCheckingConfig {
pub format: TypeCheckingFormat,
pub color: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeCheckState {
pub committed_stubs: String,
pub pending_snippet: Option<String>,
pub config: TypeCheckingConfig,
#[serde(default)]
pub committed_imports: String,
#[serde(default)]
pub pending_imports: String,
#[serde(default)]
pub asked_modules: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModuleStub {
module: String,
source: String,
}
impl ModuleStub {
pub fn new(module: impl Into<String>, source: impl Into<String>) -> Result<Self, ModuleStubError> {
let module = module.into();
let mut parts = module.split('.');
let top = parts.next().unwrap_or_default();
validate_module_name(top).map_err(|_| invalid_path(&module, top))?;
if CHECKER_MODULE_NAMES.contains(&top) {
return Err(ModuleStubError::ReservedName(top.to_owned()));
}
if let Some(part) = parts.find(|part| !is_identifier(part)) {
return Err(invalid_path(&module, part));
}
Ok(Self {
module,
source: source.into(),
})
}
#[must_use]
pub fn module(&self) -> &str {
&self.module
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
}
pub fn validate_module_name(name: &str) -> Result<(), ModuleStubError> {
if !is_identifier(name) {
Err(ModuleStubError::InvalidName(name.to_owned()))
} else if SANDBOX_MODULE_NAMES.contains(&name) {
Err(ModuleStubError::ReservedName(name.to_owned()))
} else {
Ok(())
}
}
fn invalid_path(path: &str, part: &str) -> ModuleStubError {
if SANDBOX_MODULE_NAMES.contains(&part) {
ModuleStubError::ReservedName(part.to_owned())
} else {
ModuleStubError::InvalidName(path.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModuleStubError {
InvalidName(String),
ReservedName(String),
}
impl Display for ModuleStubError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidName(name) => write!(f, "module name {name:?} is not a valid identifier"),
Self::ReservedName(name) => write!(
f,
"module {name:?} is provided by the sandbox or its type checker and cannot be replaced"
),
}
}
}
impl Error for ModuleStubError {}
pub const SANDBOX_MODULE_NAMES: &[&str] = &[
"annotationlib",
"asyncio",
"base64",
"binascii",
"bisect",
"collections",
"copy",
"csv",
"dataclasses",
"datetime",
"decimal",
"fnmatch",
"functools",
"gc",
"hashlib",
"itertools",
"json",
"math",
"os",
"pathlib",
"posixpath",
"random",
"re",
"sys",
"time",
"tomllib",
"typing",
"unicodedata",
];
pub const CHECKER_MODULE_NAMES: &[&str] = &[
"__future__",
"__main__",
"_collections_abc",
"_io",
"_typeshed",
"abc",
"builtins",
"enum",
"genericpath",
"io",
"ty_extensions",
"types",
"typing_extensions",
];