use rustpython_parser::ast::{self, Expr, Stmt};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintWarning {
pub offset: usize,
pub code: String,
pub message: String,
pub severity: Severity,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
Severity::Info => write!(f, "info"),
}
}
}
pub struct DepylintAnalyzer {
warnings: Vec<LintWarning>,
reported: HashSet<String>,
}
impl Default for DepylintAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl DepylintAnalyzer {
pub fn new() -> Self {
Self {
warnings: Vec::new(),
reported: HashSet::new(),
}
}
pub fn analyze(&mut self, source: &str) -> Vec<LintWarning> {
self.warnings.clear();
self.reported.clear();
let parsed = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<input>");
if let Ok(ast::Mod::Module(module)) = parsed {
for stmt in &module.body {
self.visit_stmt(stmt);
}
}
std::mem::take(&mut self.warnings)
}
fn get_offset(range: &ast::text_size::TextRange) -> usize {
range.start().into()
}
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::FunctionDef(func) => {
if func.name.as_str().starts_with("__") && func.name.as_str().ends_with("__") {
self.check_dunder_method(&func.name, Self::get_offset(&func.range));
}
for s in &func.body {
self.visit_stmt(s);
}
}
Stmt::AsyncFunctionDef(func) => {
for s in &func.body {
self.visit_stmt(s);
}
}
Stmt::ClassDef(class) => {
for keyword in &class.keywords {
if keyword
.arg
.as_ref()
.is_some_and(|a| a.as_str() == "metaclass")
{
self.add_warning(
Self::get_offset(&class.range),
"DPL003",
"Metaclasses are not supported",
Severity::Error,
Some("Use composition or factory patterns instead".to_string()),
);
}
}
for s in &class.body {
self.visit_stmt(s);
}
}
Stmt::Expr(expr_stmt) => {
self.check_self_reference(stmt);
self.visit_expr(&expr_stmt.value);
}
Stmt::Assign(assign) => {
self.check_self_reference(stmt);
self.visit_expr(&assign.value);
for target in &assign.targets {
self.visit_expr(target);
}
}
Stmt::AnnAssign(ann_assign) => {
if let Some(value) = &ann_assign.value {
self.visit_expr(value);
}
}
Stmt::Return(ret) => {
if let Some(value) = &ret.value {
self.visit_expr(value);
}
}
Stmt::If(if_stmt) => {
self.visit_expr(&if_stmt.test);
for s in &if_stmt.body {
self.visit_stmt(s);
}
for s in &if_stmt.orelse {
self.visit_stmt(s);
}
}
Stmt::For(for_stmt) => {
self.check_mutation_while_iterating(for_stmt);
self.visit_expr(&for_stmt.iter);
for s in &for_stmt.body {
self.visit_stmt(s);
}
}
Stmt::While(while_stmt) => {
self.visit_expr(&while_stmt.test);
for s in &while_stmt.body {
self.visit_stmt(s);
}
}
Stmt::With(with_stmt) => {
for item in &with_stmt.items {
self.visit_expr(&item.context_expr);
}
for s in &with_stmt.body {
self.visit_stmt(s);
}
}
Stmt::Try(try_stmt) => {
for s in &try_stmt.body {
self.visit_stmt(s);
}
for handler in &try_stmt.handlers {
let ast::ExceptHandler::ExceptHandler(h) = handler;
for s in &h.body {
self.visit_stmt(s);
}
}
}
_ => {}
}
}
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Call(call) => {
if let Expr::Name(name) = call.func.as_ref() {
let func_name = name.id.as_str();
let offset = Self::get_offset(&call.range);
match func_name {
"eval" => {
self.add_warning(
offset,
"DPL001",
"eval() is not supported - dynamic code execution cannot be transpiled",
Severity::Error,
Some("Refactor to use explicit logic or data structures".to_string()),
);
}
"exec" => {
self.add_warning(
offset,
"DPL002",
"exec() is not supported - dynamic code execution cannot be transpiled",
Severity::Error,
Some("Refactor to use explicit function calls".to_string()),
);
}
"globals" => {
self.add_warning(
offset,
"DPL004",
"globals() is not supported - dynamic namespace access cannot be transpiled",
Severity::Error,
Some("Use explicit module imports or pass variables as arguments".to_string()),
);
}
"locals" => {
self.add_warning(
offset,
"DPL005",
"locals() is not supported - dynamic namespace access cannot be transpiled",
Severity::Warning,
Some("Use explicit variable references".to_string()),
);
}
"setattr" | "getattr" | "delattr" => {
self.add_warning(
offset,
"DPL006",
&format!(
"{}() with dynamic attribute names is not fully supported",
func_name
),
Severity::Warning,
Some("Use explicit attribute access when possible".to_string()),
);
}
"type" if call.args.len() == 3 => {
self.add_warning(
offset,
"DPL007",
"Dynamic class creation with type() is not supported",
Severity::Error,
Some("Define classes statically".to_string()),
);
}
_ => {}
}
}
for arg in &call.args {
self.visit_expr(arg);
}
}
Expr::BinOp(binop) => {
self.visit_expr(&binop.left);
self.visit_expr(&binop.right);
}
Expr::UnaryOp(unop) => {
self.visit_expr(&unop.operand);
}
Expr::Lambda(lambda) => {
self.visit_expr(&lambda.body);
}
Expr::IfExp(ifexp) => {
self.visit_expr(&ifexp.test);
self.visit_expr(&ifexp.body);
self.visit_expr(&ifexp.orelse);
}
Expr::List(list) => {
for elt in &list.elts {
self.visit_expr(elt);
}
}
Expr::Dict(dict) => {
for key in dict.keys.iter().flatten() {
self.visit_expr(key);
}
for value in &dict.values {
self.visit_expr(value);
}
}
Expr::ListComp(comp) => {
self.visit_expr(&comp.elt);
for gen in &comp.generators {
self.visit_expr(&gen.iter);
}
}
Expr::DictComp(comp) => {
self.visit_expr(&comp.key);
self.visit_expr(&comp.value);
for gen in &comp.generators {
self.visit_expr(&gen.iter);
}
}
Expr::Subscript(subscript) => {
self.visit_expr(&subscript.value);
self.visit_expr(&subscript.slice);
}
Expr::Attribute(attr) => {
self.visit_expr(&attr.value);
}
Expr::Compare(compare) => {
self.visit_expr(&compare.left);
for comp in &compare.comparators {
self.visit_expr(comp);
}
}
_ => {}
}
}
fn check_dunder_method(&mut self, name: &str, offset: usize) {
let problematic_dunders = [
("__getattr__", "DPL008", "Dynamic attribute access"),
("__setattr__", "DPL009", "Dynamic attribute setting"),
("__delattr__", "DPL010", "Dynamic attribute deletion"),
(
"__getattribute__",
"DPL011",
"Attribute access interception",
),
];
for (dunder, code, desc) in problematic_dunders {
if name == dunder {
self.add_warning(
offset,
code,
&format!("{} ({}) is not fully supported", dunder, desc),
Severity::Warning,
Some("Use explicit properties or methods".to_string()),
);
}
}
}
fn add_warning(
&mut self,
offset: usize,
code: &str,
message: &str,
severity: Severity,
suggestion: Option<String>,
) {
let key = format!("{}:{}", code, offset);
if !self.reported.contains(&key) {
self.reported.insert(key);
self.warnings.push(LintWarning {
offset,
code: code.to_string(),
message: message.to_string(),
severity,
suggestion,
});
}
}
pub fn check_mutation_while_iterating(&mut self, for_stmt: &ast::StmtFor) {
let iter_name = match &*for_stmt.iter {
Expr::Name(name) => Some(name.id.as_str().to_string()),
_ => None,
};
if let Some(iter_var) = iter_name {
for stmt in &for_stmt.body {
if self.stmt_mutates_var(stmt, &iter_var) {
self.add_warning(
Self::get_offset(&for_stmt.range),
"DPL100",
&format!(
"Mutating '{}' while iterating over it is not supported - \
Rust's borrow checker prevents this pattern",
iter_var
),
Severity::Error,
Some(format!(
"Collect items to add/remove in a separate list, then modify '{}' after the loop",
iter_var
)),
);
break; }
}
}
}
fn stmt_mutates_var(&self, stmt: &Stmt, var_name: &str) -> bool {
match stmt {
Stmt::Expr(expr_stmt) => self.expr_mutates_var(&expr_stmt.value, var_name),
Stmt::If(if_stmt) => {
if_stmt
.body
.iter()
.any(|s| self.stmt_mutates_var(s, var_name))
|| if_stmt
.orelse
.iter()
.any(|s| self.stmt_mutates_var(s, var_name))
}
_ => false,
}
}
fn expr_mutates_var(&self, expr: &Expr, var_name: &str) -> bool {
match expr {
Expr::Call(call) => {
if let Expr::Attribute(attr) = call.func.as_ref() {
if let Expr::Name(name) = attr.value.as_ref() {
if name.id.as_str() == var_name {
let method = attr.attr.as_str();
let mutating_methods = [
"append",
"extend",
"insert",
"remove",
"pop",
"clear",
"add",
"discard",
"update",
"setdefault",
"__setitem__",
];
return mutating_methods.contains(&method);
}
}
}
false
}
_ => false,
}
}
pub fn check_self_reference(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Assign(assign) => {
for target in &assign.targets {
if let Expr::Subscript(subscript) = target {
if let Expr::Name(target_name) = subscript.value.as_ref() {
if let Expr::Name(value_name) = assign.value.as_ref() {
if target_name.id == value_name.id {
self.add_warning(
Self::get_offset(&assign.range),
"DPL101",
&format!(
"Self-referential assignment to '{}' is not supported - \
Rust cannot represent cyclic data structures without Rc/RefCell",
target_name.id
),
Severity::Error,
Some(
"Use indices or separate data structures to represent relationships".to_string(),
),
);
}
}
}
}
}
}
Stmt::Expr(expr_stmt) => {
if let Expr::Call(call) = expr_stmt.value.as_ref() {
if let Expr::Attribute(attr) = call.func.as_ref() {
let method = attr.attr.as_str();
if method == "append" || method == "add" || method == "extend" {
if let Expr::Name(receiver_name) = attr.value.as_ref() {
for arg in &call.args {
if let Expr::Name(arg_name) = arg {
if receiver_name.id == arg_name.id {
self.add_warning(
Self::get_offset(&call.range),
"DPL101",
&format!(
"Adding '{}' to itself creates a cyclic reference - \
Rust cannot represent this without Rc/RefCell",
receiver_name.id
),
Severity::Error,
Some(
"Use a copy if you need to store duplicate data".to_string(),
),
);
}
}
}
}
}
}
}
}
_ => {}
}
}
pub fn check_cyclic_assignment(&mut self, assigns: &[&Stmt]) {
let mut attr_assigns: Vec<(String, String, String)> = Vec::new();
for stmt in assigns {
if let Stmt::Assign(assign) = stmt {
for target in &assign.targets {
if let Expr::Attribute(attr) = target {
if let Expr::Name(obj_name) = attr.value.as_ref() {
if let Expr::Name(value_name) = assign.value.as_ref() {
let obj = obj_name.id.as_str().to_string();
let field = attr.attr.as_str().to_string();
let value = value_name.id.as_str().to_string();
attr_assigns.push((obj, field, value));
}
}
}
}
}
}
for (i, (obj1, _field1, val1)) in attr_assigns.iter().enumerate() {
for (obj2, _field2, val2) in attr_assigns.iter().skip(i + 1) {
if obj1 == val2 && obj2 == val1 {
self.add_warning(
0, "DPL102",
&format!(
"Cyclic reference detected: '{}' and '{}' reference each other - \
Rust cannot represent cyclic structures without Rc/RefCell",
obj1, obj2
),
Severity::Error,
Some("Use weak references or restructure to avoid cycles".to_string()),
);
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PokaYokeViolation {
pub code: String,
pub description: String,
pub offset: usize,
pub suggestion: String,
}
impl std::fmt::Display for PokaYokeViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.description)
}
}
impl std::error::Error for PokaYokeViolation {}
pub fn check_poka_yoke(func: &depyler_hir::hir::HirFunction) -> Result<(), PokaYokeViolation> {
for stmt in &func.body {
if let Some(violation) = detect_hir_mutation_while_iterating(stmt) {
return Err(violation);
}
if let Some(violation) = detect_hir_self_reference(stmt) {
return Err(violation);
}
}
Ok(())
}
fn detect_hir_mutation_while_iterating(stmt: &depyler_hir::hir::HirStmt) -> Option<PokaYokeViolation> {
use depyler_hir::hir::{HirExpr, HirStmt};
if let HirStmt::For { iter, body, .. } = stmt {
let iter_name = if let HirExpr::Var(name) = iter {
Some(name.clone())
} else {
None
};
if let Some(iter_var) = iter_name {
for body_stmt in body {
if hir_stmt_mutates_var(body_stmt, &iter_var) {
return Some(PokaYokeViolation {
code: "DPL100".to_string(),
description: format!(
"Cannot mutate '{}' while iterating over it",
iter_var
),
offset: 0,
suggestion: "Collect modifications and apply after loop".to_string(),
});
}
}
}
}
None
}
fn hir_stmt_mutates_var(stmt: &depyler_hir::hir::HirStmt, var_name: &str) -> bool {
use depyler_hir::hir::HirStmt;
match stmt {
HirStmt::Expr(expr) => hir_expr_mutates_var(expr, var_name),
HirStmt::If {
then_body,
else_body,
..
} => {
let then_mutates = then_body.iter().any(|s| hir_stmt_mutates_var(s, var_name));
let else_mutates = else_body
.as_ref()
.map(|stmts| stmts.iter().any(|s| hir_stmt_mutates_var(s, var_name)))
.unwrap_or(false);
then_mutates || else_mutates
}
HirStmt::Block(stmts) => stmts.iter().any(|s| hir_stmt_mutates_var(s, var_name)),
_ => false,
}
}
fn hir_expr_mutates_var(expr: &depyler_hir::hir::HirExpr, var_name: &str) -> bool {
use depyler_hir::hir::HirExpr;
match expr {
HirExpr::MethodCall { object, method, .. } => {
if let HirExpr::Var(name) = object.as_ref() {
if name == var_name {
let mutating_methods = [
"append",
"extend",
"insert",
"remove",
"pop",
"clear",
"add",
"discard",
"update",
"setdefault",
];
return mutating_methods.contains(&method.as_str());
}
}
false
}
_ => false,
}
}
fn detect_hir_self_reference(stmt: &depyler_hir::hir::HirStmt) -> Option<PokaYokeViolation> {
use depyler_hir::hir::{HirExpr, HirStmt};
match stmt {
HirStmt::Assign { target, value, .. } => check_self_assign(target, value),
HirStmt::Expr(HirExpr::MethodCall {
object,
method,
args,
..
}) => check_self_method_call(object, method, args),
_ => None,
}
}
fn check_self_assign(
target: &depyler_hir::hir::AssignTarget,
value: &depyler_hir::hir::HirExpr,
) -> Option<PokaYokeViolation> {
let target_var = find_base_var_in_assign(target)?;
let depyler_hir::hir::HirExpr::Var(value_var) = value else {
return None;
};
if target_var != value_var {
return None;
}
Some(PokaYokeViolation {
code: "DPL101".to_string(),
description: format!("Self-referential assignment to '{target_var}' is not allowed"),
offset: 0,
suggestion: "Use indices or separate structures".to_string(),
})
}
fn check_self_method_call(
object: &depyler_hir::hir::HirExpr,
method: &str,
args: &[depyler_hir::hir::HirExpr],
) -> Option<PokaYokeViolation> {
if !matches!(method, "append" | "add" | "extend") {
return None;
}
let depyler_hir::hir::HirExpr::Var(recv_name) = object else {
return None;
};
let self_arg = args.iter().find(|arg| {
matches!(arg, depyler_hir::hir::HirExpr::Var(name) if name == recv_name)
});
self_arg.map(|_| PokaYokeViolation {
code: "DPL101".to_string(),
description: format!("Adding '{recv_name}' to itself creates a cycle"),
offset: 0,
suggestion: "Use a copy to store duplicate data".to_string(),
})
}
fn find_base_var_in_assign(target: &depyler_hir::hir::AssignTarget) -> Option<&str> {
use depyler_hir::hir::{AssignTarget, HirExpr};
match target {
AssignTarget::Symbol(name) => Some(name.as_str()),
AssignTarget::Index { base, .. } => {
if let HirExpr::Var(name) = base.as_ref() {
Some(name.as_str())
} else {
None
}
}
AssignTarget::Attribute { value, .. } => {
if let HirExpr::Var(name) = value.as_ref() {
Some(name.as_str())
} else {
None
}
}
AssignTarget::Tuple(_) => None, }
}
pub fn format_warnings(warnings: &[LintWarning], source: &str, source_path: &str) -> String {
let mut output = String::new();
for w in warnings {
let (line, col) = offset_to_line_col(source, w.offset);
output.push_str(&format!(
"{}:{}:{}: {} [{}]: {}\n",
source_path, line, col, w.severity, w.code, w.message
));
if let Some(ref suggestion) = w.suggestion {
output.push_str(&format!(" suggestion: {}\n", suggestion));
}
}
output
}
fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
for (i, c) in source.char_indices() {
if i >= offset {
break;
}
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lint_warning_new() {
let warning = LintWarning {
offset: 10,
code: "DPL001".to_string(),
message: "test message".to_string(),
severity: Severity::Error,
suggestion: Some("fix it".to_string()),
};
assert_eq!(warning.offset, 10);
assert_eq!(warning.code, "DPL001");
assert_eq!(warning.message, "test message");
assert_eq!(warning.severity, Severity::Error);
assert_eq!(warning.suggestion, Some("fix it".to_string()));
}
#[test]
fn test_lint_warning_no_suggestion() {
let warning = LintWarning {
offset: 0,
code: "DPL000".to_string(),
message: "msg".to_string(),
severity: Severity::Info,
suggestion: None,
};
assert!(warning.suggestion.is_none());
}
#[test]
fn test_lint_warning_clone() {
let warning = LintWarning {
offset: 5,
code: "DPL002".to_string(),
message: "exec".to_string(),
severity: Severity::Warning,
suggestion: None,
};
let cloned = warning.clone();
assert_eq!(warning, cloned);
}
#[test]
fn test_lint_warning_partial_eq() {
let w1 = LintWarning {
offset: 1,
code: "A".to_string(),
message: "m".to_string(),
severity: Severity::Error,
suggestion: None,
};
let w2 = LintWarning {
offset: 1,
code: "A".to_string(),
message: "m".to_string(),
severity: Severity::Error,
suggestion: None,
};
assert_eq!(w1, w2);
}
#[test]
fn test_lint_warning_debug() {
let warning = LintWarning {
offset: 0,
code: "DPL001".to_string(),
message: "test".to_string(),
severity: Severity::Error,
suggestion: None,
};
let debug = format!("{:?}", warning);
assert!(debug.contains("LintWarning"));
assert!(debug.contains("DPL001"));
}
#[test]
fn test_severity_display_error() {
assert_eq!(format!("{}", Severity::Error), "error");
}
#[test]
fn test_severity_display_warning() {
assert_eq!(format!("{}", Severity::Warning), "warning");
}
#[test]
fn test_severity_display_info() {
assert_eq!(format!("{}", Severity::Info), "info");
}
#[test]
fn test_severity_clone() {
let s = Severity::Error;
let cloned = s;
assert_eq!(s, cloned);
}
#[test]
fn test_severity_copy() {
let s1 = Severity::Warning;
let s2 = s1;
assert_eq!(s1, s2);
}
#[test]
fn test_severity_debug() {
let debug = format!("{:?}", Severity::Info);
assert!(debug.contains("Info"));
}
#[test]
fn test_severity_partial_eq() {
assert_eq!(Severity::Error, Severity::Error);
assert_ne!(Severity::Error, Severity::Warning);
assert_ne!(Severity::Warning, Severity::Info);
}
#[test]
fn test_analyzer_new() {
let analyzer = DepylintAnalyzer::new();
assert!(analyzer.warnings.is_empty());
assert!(analyzer.reported.is_empty());
}
#[test]
fn test_analyzer_default() {
let analyzer = DepylintAnalyzer::default();
assert!(analyzer.warnings.is_empty());
}
#[test]
fn test_analyzer_reuse() {
let mut analyzer = DepylintAnalyzer::new();
let w1 = analyzer.analyze("eval('1')");
assert_eq!(w1.len(), 1);
let w2 = analyzer.analyze("x = 1");
assert!(w2.is_empty());
}
#[test]
fn test_detect_eval() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("result = eval('1 + 2')");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL001");
assert_eq!(warnings[0].severity, Severity::Error);
}
#[test]
fn test_detect_exec() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("exec('print(1)')");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL002");
}
#[test]
fn test_detect_metaclass() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("class Foo(metaclass=ABCMeta): pass");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL003");
}
#[test]
fn test_detect_globals() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("x = globals()['foo']");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL004");
}
#[test]
fn test_detect_locals() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("x = locals()");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL005");
assert_eq!(warnings[0].severity, Severity::Warning);
}
#[test]
fn test_detect_setattr() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("setattr(obj, 'name', value)");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL006");
}
#[test]
fn test_detect_getattr_func() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("x = getattr(obj, 'name')");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL006");
}
#[test]
fn test_detect_delattr() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("delattr(obj, 'name')");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL006");
}
#[test]
fn test_detect_getattr_dunder() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
class Foo:
def __getattr__(self, name):
return None
"#,
);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL008");
}
#[test]
fn test_detect_setattr_dunder() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
class Foo:
def __setattr__(self, name, value):
pass
"#,
);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL009");
}
#[test]
fn test_detect_delattr_dunder() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
class Foo:
def __delattr__(self, name):
pass
"#,
);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL010");
}
#[test]
fn test_detect_getattribute_dunder() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
class Foo:
def __getattribute__(self, name):
return object.__getattribute__(self, name)
"#,
);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL011");
}
#[test]
fn test_clean_code_no_warnings() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
def add(a: int, b: int) -> int:
return a + b
class Calculator:
def multiply(self, x: float, y: float) -> float:
return x * y
"#,
);
assert!(warnings.is_empty(), "Clean code should have no warnings");
}
#[test]
fn test_dynamic_type_creation() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("MyClass = type('MyClass', (Base,), {'x': 1})");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL007");
}
#[test]
fn test_type_single_arg_ok() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("t = type(obj)");
assert!(warnings.is_empty());
}
#[test]
fn test_no_duplicate_warnings() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("eval('1')");
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_multiple_different_warnings() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
eval('1')
exec('2')
globals()
"#,
);
assert_eq!(warnings.len(), 3);
}
#[test]
fn test_nested_in_function() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
def foo():
return eval('x')
"#,
);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "DPL001");
}
#[test]
fn test_nested_in_class() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
class Foo:
def method(self):
exec('pass')
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_if_statement() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
if True:
eval('1')
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_for_loop() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
for i in range(10):
eval(str(i))
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_while_loop() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
while True:
exec('break')
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_try_block() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
try:
eval('bad')
except:
pass
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_with_statement() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
with open('f') as f:
eval(f.read())
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_lambda() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("f = lambda x: eval(x)");
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_list_comprehension() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("[eval(x) for x in items]");
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_dict_comprehension() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("{k: eval(v) for k, v in items}");
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_in_ternary() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("x = eval('1') if cond else 0");
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_async_function() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
async def foo():
return eval('x')
"#,
);
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_format_warnings() {
let source = "result = eval('1 + 2')";
let warnings = vec![LintWarning {
offset: 9,
code: "DPL001".to_string(),
message: "eval() is not supported".to_string(),
severity: Severity::Error,
suggestion: Some("Use explicit logic".to_string()),
}];
let output = format_warnings(&warnings, source, "test.py");
assert!(output.contains("test.py:1:"));
assert!(output.contains("DPL001"));
assert!(output.contains("suggestion:"));
}
#[test]
fn test_format_warnings_no_suggestion() {
let source = "exec('1')";
let warnings = vec![LintWarning {
offset: 0,
code: "DPL002".to_string(),
message: "exec".to_string(),
severity: Severity::Error,
suggestion: None,
}];
let output = format_warnings(&warnings, source, "file.py");
assert!(!output.contains("suggestion:"));
}
#[test]
fn test_format_warnings_empty() {
let output = format_warnings(&[], "x = 1", "file.py");
assert!(output.is_empty());
}
#[test]
fn test_format_warnings_multiple() {
let source = "eval('1')\nexec('2')";
let warnings = vec![
LintWarning {
offset: 0,
code: "DPL001".to_string(),
message: "eval".to_string(),
severity: Severity::Error,
suggestion: None,
},
LintWarning {
offset: 10,
code: "DPL002".to_string(),
message: "exec".to_string(),
severity: Severity::Error,
suggestion: None,
},
];
let output = format_warnings(&warnings, source, "test.py");
assert!(output.contains("DPL001"));
assert!(output.contains("DPL002"));
}
#[test]
fn test_offset_to_line_col() {
let source = "line1\nline2\nline3";
assert_eq!(offset_to_line_col(source, 0), (1, 1)); assert_eq!(offset_to_line_col(source, 6), (2, 1)); assert_eq!(offset_to_line_col(source, 12), (3, 1)); }
#[test]
fn test_offset_to_line_col_empty() {
assert_eq!(offset_to_line_col("", 0), (1, 1));
}
#[test]
fn test_offset_to_line_col_single_line() {
let source = "hello world";
assert_eq!(offset_to_line_col(source, 0), (1, 1));
assert_eq!(offset_to_line_col(source, 5), (1, 6));
}
#[test]
fn test_offset_to_line_col_end_of_line() {
let source = "abc\ndef";
assert_eq!(offset_to_line_col(source, 3), (1, 4)); assert_eq!(offset_to_line_col(source, 4), (2, 1)); }
#[test]
fn test_offset_to_line_col_beyond_end() {
let source = "ab";
assert_eq!(offset_to_line_col(source, 100), (1, 3));
}
#[test]
fn test_parse_error_graceful() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("def broken(");
assert!(warnings.is_empty()); }
#[test]
fn test_empty_source() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("");
assert!(warnings.is_empty());
}
#[test]
fn test_normal_dunder_ok() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze(
r#"
class Foo:
def __init__(self):
pass
def __str__(self):
return ""
"#,
);
assert!(warnings.is_empty());
}
#[test]
fn test_suggestions_present() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("eval('1')");
assert!(warnings[0].suggestion.is_some());
}
#[test]
fn test_offset_is_nonzero() {
let mut analyzer = DepylintAnalyzer::new();
let warnings = analyzer.analyze("x = 1\neval('2')");
assert!(warnings[0].offset > 0);
}
#[test]
fn test_s12_poka_yoke_violation_debug() {
let v = PokaYokeViolation {
code: "DPL100".to_string(),
description: "test".to_string(),
offset: 42,
suggestion: "fix it".to_string(),
};
let debug = format!("{:?}", v);
assert!(debug.contains("DPL100"));
}
#[test]
fn test_s12_poka_yoke_violation_clone() {
let v = PokaYokeViolation {
code: "DPL101".to_string(),
description: "desc".to_string(),
offset: 0,
suggestion: "suggest".to_string(),
};
let cloned = v.clone();
assert_eq!(cloned.code, "DPL101");
assert_eq!(cloned.description, "desc");
}
fn make_test_func(name: &str, body: Vec<depyler_hir::hir::HirStmt>) -> depyler_hir::hir::HirFunction {
use depyler_hir::hir::{FunctionProperties, HirFunction, Type};
use depyler_annotations::TranspilationAnnotations;
use smallvec::smallvec;
HirFunction {
name: name.to_string(),
params: smallvec![],
ret_type: Type::None,
body,
properties: FunctionProperties::default(),
annotations: TranspilationAnnotations::default(),
docstring: None,
}
}
#[test]
fn test_s12_check_poka_yoke_clean_function() {
use depyler_hir::hir::{HirExpr, HirStmt};
let func = make_test_func(
"clean",
vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
);
let result = check_poka_yoke(&func);
assert!(result.is_ok());
}
#[test]
fn test_s12_check_poka_yoke_mutation_while_iterating() {
use depyler_hir::hir::{AssignTarget, HirExpr, HirStmt};
let func = make_test_func(
"bad_loop",
vec![HirStmt::For {
target: AssignTarget::Symbol("x".to_string()),
iter: HirExpr::Var("items".to_string()),
body: vec![HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("items".to_string())),
method: "append".to_string(),
args: vec![HirExpr::Var("x".to_string())],
kwargs: vec![],
})],
}],
);
let result = check_poka_yoke(&func);
assert!(result.is_err());
let violation = result.unwrap_err();
assert_eq!(violation.code, "DPL100");
assert!(violation.description.contains("items"));
}
#[test]
fn test_s12_check_poka_yoke_self_reference_assign() {
use depyler_hir::hir::{AssignTarget, HirExpr, HirStmt, Literal};
let func = make_test_func(
"self_ref",
vec![HirStmt::Assign {
target: AssignTarget::Index {
base: Box::new(HirExpr::Var("d".to_string())),
index: Box::new(HirExpr::Literal(Literal::String("key".to_string()))),
},
value: HirExpr::Var("d".to_string()),
type_annotation: None,
}],
);
let result = check_poka_yoke(&func);
assert!(result.is_err());
let violation = result.unwrap_err();
assert_eq!(violation.code, "DPL101");
}
#[test]
fn test_s12_check_poka_yoke_self_append() {
use depyler_hir::hir::{HirExpr, HirStmt};
let func = make_test_func(
"self_append",
vec![HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("lst".to_string())),
method: "append".to_string(),
args: vec![HirExpr::Var("lst".to_string())],
kwargs: vec![],
})],
);
let result = check_poka_yoke(&func);
assert!(result.is_err());
let violation = result.unwrap_err();
assert_eq!(violation.code, "DPL101");
assert!(violation.description.contains("lst"));
}
#[test]
fn test_s12_hir_stmt_mutates_var_if_branch() {
use depyler_hir::hir::{HirExpr, HirStmt, Literal};
let stmt = HirStmt::If {
condition: HirExpr::Literal(Literal::Bool(true)),
then_body: vec![HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("items".to_string())),
method: "pop".to_string(),
args: vec![],
kwargs: vec![],
})],
else_body: None,
};
assert!(hir_stmt_mutates_var(&stmt, "items"));
}
#[test]
fn test_s12_hir_stmt_mutates_var_else_branch() {
use depyler_hir::hir::{HirExpr, HirStmt, Literal};
let stmt = HirStmt::If {
condition: HirExpr::Literal(Literal::Bool(false)),
then_body: vec![],
else_body: Some(vec![HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("items".to_string())),
method: "clear".to_string(),
args: vec![],
kwargs: vec![],
})]),
};
assert!(hir_stmt_mutates_var(&stmt, "items"));
}
#[test]
fn test_s12_hir_stmt_mutates_var_block() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Block(vec![HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "add".to_string(),
args: vec![HirExpr::Var("x".to_string())],
kwargs: vec![],
})]);
assert!(hir_stmt_mutates_var(&stmt, "s"));
}
#[test]
fn test_s12_hir_stmt_mutates_var_no_mutation() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("items".to_string())),
method: "len".to_string(),
args: vec![],
kwargs: vec![],
});
assert!(!hir_stmt_mutates_var(&stmt, "items"));
}
#[test]
fn test_s12_hir_stmt_mutates_var_different_var() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("other".to_string())),
method: "append".to_string(),
args: vec![],
kwargs: vec![],
});
assert!(!hir_stmt_mutates_var(&stmt, "items"));
}
#[test]
fn test_s12_hir_expr_mutates_all_methods() {
use depyler_hir::hir::HirExpr;
let methods = [
"append", "extend", "insert", "remove", "pop", "clear", "add", "discard", "update",
"setdefault",
];
for method in methods {
let expr = HirExpr::MethodCall {
object: Box::new(HirExpr::Var("x".to_string())),
method: method.to_string(),
args: vec![],
kwargs: vec![],
};
assert!(
hir_expr_mutates_var(&expr, "x"),
"Method '{}' should be detected as mutation",
method
);
}
}
#[test]
fn test_s12_hir_expr_non_mutating_method() {
use depyler_hir::hir::HirExpr;
let expr = HirExpr::MethodCall {
object: Box::new(HirExpr::Var("x".to_string())),
method: "get".to_string(),
args: vec![],
kwargs: vec![],
};
assert!(!hir_expr_mutates_var(&expr, "x"));
}
#[test]
fn test_s12_hir_expr_non_method_call() {
use depyler_hir::hir::HirExpr;
let expr = HirExpr::Var("x".to_string());
assert!(!hir_expr_mutates_var(&expr, "x"));
}
#[test]
fn test_s12_find_base_var_symbol() {
use depyler_hir::hir::AssignTarget;
let target = AssignTarget::Symbol("x".to_string());
assert_eq!(find_base_var_in_assign(&target), Some("x"));
}
#[test]
fn test_s12_find_base_var_index() {
use depyler_hir::hir::{AssignTarget, HirExpr, Literal};
let target = AssignTarget::Index {
base: Box::new(HirExpr::Var("d".to_string())),
index: Box::new(HirExpr::Literal(Literal::String("key".to_string()))),
};
assert_eq!(find_base_var_in_assign(&target), Some("d"));
}
#[test]
fn test_s12_find_base_var_attribute() {
use depyler_hir::hir::{AssignTarget, HirExpr};
let target = AssignTarget::Attribute {
value: Box::new(HirExpr::Var("obj".to_string())),
attr: "field".to_string(),
};
assert_eq!(find_base_var_in_assign(&target), Some("obj"));
}
#[test]
fn test_s12_find_base_var_tuple() {
use depyler_hir::hir::AssignTarget;
let target = AssignTarget::Tuple(vec![
AssignTarget::Symbol("a".to_string()),
AssignTarget::Symbol("b".to_string()),
]);
assert_eq!(find_base_var_in_assign(&target), None);
}
#[test]
fn test_s12_find_base_var_index_non_var() {
use depyler_hir::hir::{AssignTarget, HirExpr, Literal};
let target = AssignTarget::Index {
base: Box::new(HirExpr::Literal(Literal::Int(0))),
index: Box::new(HirExpr::Literal(Literal::Int(1))),
};
assert_eq!(find_base_var_in_assign(&target), None);
}
#[test]
fn test_s12_find_base_var_attribute_non_var() {
use depyler_hir::hir::{AssignTarget, HirExpr, Literal};
let target = AssignTarget::Attribute {
value: Box::new(HirExpr::Literal(Literal::Int(0))),
attr: "field".to_string(),
};
assert_eq!(find_base_var_in_assign(&target), None);
}
#[test]
fn test_s12_detect_hir_self_reference_no_match() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("a".to_string())),
method: "append".to_string(),
args: vec![HirExpr::Var("b".to_string())],
kwargs: vec![],
});
assert!(detect_hir_self_reference(&stmt).is_none());
}
#[test]
fn test_s12_detect_hir_self_reference_extend() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("lst".to_string())),
method: "extend".to_string(),
args: vec![HirExpr::Var("lst".to_string())],
kwargs: vec![],
});
let result = detect_hir_self_reference(&stmt);
assert!(result.is_some());
assert_eq!(result.unwrap().code, "DPL101");
}
#[test]
fn test_s12_detect_hir_self_reference_add() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("s".to_string())),
method: "add".to_string(),
args: vec![HirExpr::Var("s".to_string())],
kwargs: vec![],
});
let result = detect_hir_self_reference(&stmt);
assert!(result.is_some());
}
#[test]
fn test_s12_detect_hir_mutation_no_iter() {
use depyler_hir::hir::{HirExpr, HirStmt, Literal};
let stmt = HirStmt::For {
target: depyler_hir::hir::AssignTarget::Symbol("x".to_string()),
iter: HirExpr::Literal(Literal::Int(10)),
body: vec![],
};
assert!(detect_hir_mutation_while_iterating(&stmt).is_none());
}
#[test]
fn test_s12_detect_hir_mutation_safe_loop() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::For {
target: depyler_hir::hir::AssignTarget::Symbol("x".to_string()),
iter: HirExpr::Var("items".to_string()),
body: vec![HirStmt::Expr(HirExpr::MethodCall {
object: Box::new(HirExpr::Var("result".to_string())),
method: "append".to_string(),
args: vec![HirExpr::Var("x".to_string())],
kwargs: vec![],
})],
};
assert!(detect_hir_mutation_while_iterating(&stmt).is_none());
}
#[test]
fn test_s12_detect_hir_mutation_not_for_stmt() {
use depyler_hir::hir::{HirExpr, HirStmt};
let stmt = HirStmt::Expr(HirExpr::Var("x".to_string()));
assert!(detect_hir_mutation_while_iterating(&stmt).is_none());
}
#[test]
fn test_s12_check_cyclic_assignment_basic() {
let source = r#"
class Node:
pass
a = Node()
b = Node()
a.next = b
b.next = a
"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
let stmts: Vec<&Stmt> = module.body.iter().collect();
analyzer.check_cyclic_assignment(&stmts);
assert!(
!analyzer.warnings.is_empty(),
"Should detect cycle: a.next=b, b.next=a"
);
assert_eq!(analyzer.warnings[0].code, "DPL102");
}
}
#[test]
fn test_s12_check_cyclic_assignment_no_cycle() {
let source = r#"
a = 1
b = 2
"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
let stmts: Vec<&Stmt> = module.body.iter().collect();
analyzer.check_cyclic_assignment(&stmts);
assert!(
analyzer.warnings.is_empty(),
"Should not detect cycle in simple assignments"
);
}
}
#[test]
fn test_s12_format_warnings() {
let warnings = vec![
LintWarning {
offset: 0,
code: "DPL001".to_string(),
message: "eval detected".to_string(),
severity: Severity::Error,
suggestion: Some("Use literal".to_string()),
},
LintWarning {
offset: 10,
code: "DPL002".to_string(),
message: "exec detected".to_string(),
severity: Severity::Warning,
suggestion: None,
},
];
let output = format_warnings(&warnings, "eval('1')\nexec('x')", "test.py");
assert!(output.contains("test.py"));
assert!(output.contains("DPL001"));
assert!(output.contains("DPL002"));
assert!(output.contains("suggestion: Use literal"));
}
#[test]
fn test_s12_format_warnings_empty() {
let output = format_warnings(&[], "x = 1", "test.py");
assert!(output.is_empty());
}
#[test]
fn test_s12_check_mutation_while_iterating_ast() {
let source = r#"
for x in items:
items.append(x * 2)
"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
if let Stmt::For(for_stmt) = stmt {
analyzer.check_mutation_while_iterating(for_stmt);
}
}
assert!(!analyzer.warnings.is_empty());
assert_eq!(analyzer.warnings[0].code, "DPL100");
}
}
#[test]
fn test_s12_check_mutation_while_iterating_safe() {
let source = r#"
for x in items:
result.append(x)
"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
if let Stmt::For(for_stmt) = stmt {
analyzer.check_mutation_while_iterating(for_stmt);
}
}
assert!(analyzer.warnings.is_empty());
}
}
#[test]
fn test_s12_check_self_reference_dict() {
let source = r#"d["key"] = d"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
analyzer.check_self_reference(stmt);
}
assert!(!analyzer.warnings.is_empty());
assert_eq!(analyzer.warnings[0].code, "DPL101");
}
}
#[test]
fn test_s12_check_self_reference_list_append() {
let source = r#"lst.append(lst)"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
analyzer.check_self_reference(stmt);
}
assert!(!analyzer.warnings.is_empty());
assert_eq!(analyzer.warnings[0].code, "DPL101");
}
}
#[test]
fn test_s12_check_self_reference_no_match() {
let source = r#"a.append(b)"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
analyzer.check_self_reference(stmt);
}
assert!(analyzer.warnings.is_empty());
}
}
#[test]
fn test_s12_check_self_reference_other_stmt() {
let source = r#"x = 1"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
analyzer.check_self_reference(stmt);
}
assert!(analyzer.warnings.is_empty());
}
}
#[test]
fn test_s12_mutation_in_if_branch() {
let source = r#"
for x in items:
if x > 0:
items.remove(x)
"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
if let Stmt::For(for_stmt) = stmt {
analyzer.check_mutation_while_iterating(for_stmt);
}
}
assert!(!analyzer.warnings.is_empty());
assert_eq!(analyzer.warnings[0].code, "DPL100");
}
}
#[test]
fn test_s12_mutation_non_name_iter() {
let source = r#"
for x in get_items():
pass
"#;
let mut analyzer = DepylintAnalyzer::new();
let ast = rustpython_parser::parse(source, rustpython_parser::Mode::Module, "<test>")
.expect("parse");
if let rustpython_parser::ast::Mod::Module(module) = ast {
for stmt in &module.body {
if let Stmt::For(for_stmt) = stmt {
analyzer.check_mutation_while_iterating(for_stmt);
}
}
assert!(analyzer.warnings.is_empty());
}
}
}