architecture_component!(CodeLintPolicy);
use crate::code_lint::ast::LiteralValue;
use crate::code_lint::ast::python::{
CollectionShape, PythonCollectionType, PythonFunctionSignature, is_interface_method,
};
#[must_use]
pub(super) fn is_exempt_from_signature_rules(signature: &PythonFunctionSignature<'_>) -> bool {
signature.has_imposed_signature() || is_interface_method(signature)
}
#[must_use]
pub(super) fn is_exempt_from_body_usage_rules(signature: &PythonFunctionSignature<'_>) -> bool {
is_exempt_from_signature_rules(signature) || signature.has_stub_body()
}
#[must_use]
pub(super) fn is_fake_class_name(name: &str) -> bool {
name.trim_start_matches('_')
.strip_prefix("Fake")
.is_some_and(|rest| !rest.starts_with(|character: char| character.is_ascii_lowercase()))
}
#[must_use]
pub fn is_trivial_literal(value: &LiteralValue) -> bool {
match value {
LiteralValue::Str(content) | LiteralValue::Bytes(content) => {
let mut units = 0_usize;
let mut has_alphanumeric = false;
let mut characters = content.chars();
while let Some(character) = characters.next() {
units += 1;
if character == '\\' {
characters.next();
} else if character.is_alphanumeric() {
has_alphanumeric = true;
}
}
units < 2 || !has_alphanumeric
}
LiteralValue::Int(value) => (-1..=2).contains(value),
LiteralValue::Float(bits) => [-1.0, 0.0, 1.0, 2.0].contains(&f64::from_bits(*bits)),
}
}
#[must_use]
pub(super) fn read_only_collection_replacements(
collection_types: &[PythonCollectionType],
) -> String {
replacements_by_shape(collection_types, |shape| match shape {
CollectionShape::Mapping => "collections.abc.Mapping",
CollectionShape::Set => "collections.abc.Set",
CollectionShape::Sequence | CollectionShape::Iterable => "collections.abc.Sequence",
})
}
#[must_use]
pub(super) fn replacements_by_shape(
collection_types: &[PythonCollectionType],
replacement: impl Fn(CollectionShape) -> &'static str,
) -> String {
let mut replacements: Vec<&str> = Vec::new();
for collection_type in collection_types {
let replacement = replacement(collection_type.shape);
if !replacements.contains(&replacement) {
replacements.push(replacement);
}
}
replacements.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code_lint::ast::python::CollectionKind;
fn collection_type(
path: &str,
kind: CollectionKind,
shape: CollectionShape,
) -> PythonCollectionType {
PythonCollectionType {
path: path.to_owned(),
name: path.rsplit('.').next().unwrap_or(path).to_owned(),
kind,
shape,
}
}
#[rstest::rstest]
#[case::list(
vec![collection_type("list", CollectionKind::ConcreteMutable, CollectionShape::Sequence)],
"collections.abc.Sequence"
)]
#[case::deque(
vec![collection_type("collections.deque", CollectionKind::ConcreteMutable, CollectionShape::Sequence)],
"collections.abc.Sequence"
)]
#[case::typing_dict(
vec![collection_type("typing.Dict", CollectionKind::ConcreteMutable, CollectionShape::Mapping)],
"collections.abc.Mapping"
)]
#[case::counter(
vec![collection_type("collections.Counter", CollectionKind::ConcreteMutable, CollectionShape::Mapping)],
"collections.abc.Mapping"
)]
#[case::mutable_set(
vec![collection_type("MutableSet", CollectionKind::AbstractMutable, CollectionShape::Set)],
"collections.abc.Set"
)]
#[case::dedup_and_order(
vec![
collection_type("dict", CollectionKind::ConcreteMutable, CollectionShape::Mapping),
collection_type("list", CollectionKind::ConcreteMutable, CollectionShape::Sequence),
collection_type("OrderedDict", CollectionKind::ConcreteMutable, CollectionShape::Mapping),
],
"collections.abc.Mapping, collections.abc.Sequence"
)]
fn test_read_only_collection_replacements(
#[case] collection_types: Vec<PythonCollectionType>,
#[case] expected: &str,
) {
assert_eq!(
read_only_collection_replacements(&collection_types),
expected,
"{collection_types:?}"
);
}
#[rstest::rstest]
#[case::single_char(LiteralValue::Str("a".to_string()), true)]
#[case::delimiter(LiteralValue::Str(", ".to_string()), true)]
#[case::escaped_newline(LiteralValue::Str("\\n".to_string()), true)]
#[case::escaped_crlf(LiteralValue::Bytes("\\r\\n".to_string()), true)]
#[case::short_word(LiteralValue::Str("jj".to_string()), false)]
#[case::word_with_escape(LiteralValue::Str("a\\n".to_string()), false)]
#[case::small_integer(LiteralValue::Int(-1), true)]
#[case::integer_two(LiteralValue::Int(2), true)]
#[case::integer_minus_two(LiteralValue::Int(-2), false)]
#[case::integer_three(LiteralValue::Int(3), false)]
#[case::unit_float(LiteralValue::Float((-1.0_f64).to_bits()), true)]
#[case::float_two(LiteralValue::Float(2.0_f64.to_bits()), true)]
#[case::half_float(LiteralValue::Float(0.5_f64.to_bits()), false)]
#[case::raw_regex_class(LiteralValue::Str("\\\\s+".to_string()), false)]
fn test_is_trivial_literal(#[case] value: LiteralValue, #[case] expected: bool) {
assert_eq!(is_trivial_literal(&value), expected);
}
}