#[inline]
pub(super) fn is_valid_python_identifier(name: &str) -> bool {
let bytes = name.as_bytes();
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !(first.is_ascii_alphabetic() || first == b'_') {
return false;
}
bytes[1..]
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'_')
}
pub(super) fn parse_module_const_target(trimmed: &str) -> Option<&str> {
let bytes = trimmed.as_bytes();
let mut i = 0;
let eq_pos = loop {
if i >= bytes.len() {
return None;
}
if bytes[i] == b'=' {
let next = bytes.get(i + 1).copied().unwrap_or(b' ');
if next == b'=' {
i += 2;
continue;
}
let prev = if i > 0 { bytes[i - 1] } else { b' ' };
if matches!(
prev,
b'!' | b'<'
| b'>'
| b'+'
| b'-'
| b'*'
| b'/'
| b'%'
| b'&'
| b'|'
| b'^'
| b'@'
| b':'
| b'~'
| b'='
) {
return None;
}
break i;
}
i += 1;
};
let lhs = trimmed[..eq_pos].trim();
let name = lhs.split(':').next().unwrap_or(lhs).trim();
if is_valid_python_identifier(name) {
Some(name)
} else {
None
}
}
#[inline]
pub(super) fn bytes_match_keyword(bytes: &[u8], pos: usize, keyword: &[u8]) -> bool {
if pos + keyword.len() > bytes.len() {
return false;
}
&bytes[pos..pos + keyword.len()] == keyword
}
#[inline]
pub(super) fn extract_ascii_ident(bytes: &[u8], start: usize, end: usize) -> String {
if start >= end || end > bytes.len() {
return String::new();
}
let slice = &bytes[start..end];
if slice.iter().all(|b| b.is_ascii()) {
String::from_utf8_lossy(slice).into_owned()
} else {
String::new()
}
}
pub(super) const SKIP_BUILTINS: &[&str] = &[
"None",
"True",
"False",
"str",
"int",
"float",
"bool",
"bytes",
"list",
"dict",
"set",
"tuple",
"frozenset",
"type",
"object",
"Any",
"Union",
"Optional",
"List",
"Dict",
"Set",
"Tuple",
"Callable",
"Sequence",
"Mapping",
"Iterable",
"Iterator",
"Type",
"self",
"cls",
];
pub(super) const SKIP_TYPE_HINTS: &[&str] = &[
"None",
"True",
"False",
"str",
"int",
"float",
"bool",
"bytes",
"list",
"dict",
"set",
"tuple",
"frozenset",
"type",
"object",
"Any",
"Union",
"Optional",
"List",
"Dict",
"Set",
"Tuple",
"Callable",
"Sequence",
"Mapping",
"Iterable",
"Iterator",
"Generator",
"Coroutine",
"Awaitable",
"AsyncIterator",
"AsyncGenerator",
"Type",
"ClassVar",
"Final",
"Literal",
"TypeVar",
"Generic",
"Protocol",
"Self",
"self",
"cls",
];
pub(super) const PYTHON_KEYWORDS: &[&str] = &[
"if", "else", "elif", "while", "for", "try", "except", "finally", "with", "as", "def", "class",
"return", "yield", "raise", "import", "from", "pass", "break", "continue", "lambda", "and",
"or", "not", "in", "is", "True", "False", "None", "assert", "del", "exec", "print", "global",
"nonlocal", "async", "await",
];
pub(super) const TYPE_FACTORIES: &[&str] = &[
"defaultdict",
"Counter",
"deque",
"OrderedDict",
"ChainMap",
"namedtuple",
"TypedDict",
"NewType",
"cast",
"Depends",
"Security",
"Field",
];
#[cfg(test)]
mod tests {
use super::is_valid_python_identifier;
#[test]
fn accepts_plain_ascii_identifiers() {
assert!(is_valid_python_identifier("Foo"));
assert!(is_valid_python_identifier("foo_bar"));
assert!(is_valid_python_identifier("_private"));
assert!(is_valid_python_identifier("Foo123"));
assert!(is_valid_python_identifier("__init__"));
assert!(is_valid_python_identifier("a"));
}
#[test]
fn rejects_fstring_class_capture() {
assert!(!is_valid_python_identifier("FrameMarker {{"));
assert!(!is_valid_python_identifier("VoiceRecorder {{"));
assert!(!is_valid_python_identifier("Foo {"));
}
#[test]
fn rejects_invalid_first_chars_and_punctuation() {
assert!(!is_valid_python_identifier(""));
assert!(!is_valid_python_identifier("123abc"));
assert!(!is_valid_python_identifier("Foo Bar"));
assert!(!is_valid_python_identifier("Foo-Bar"));
assert!(!is_valid_python_identifier("Foo.Bar"));
assert!(!is_valid_python_identifier("Foo()"));
assert!(!is_valid_python_identifier(" Foo"));
}
}