use std::collections::{HashMap, HashSet};
use gazebo::variants::VariantName;
use once_cell::sync::Lazy;
use thiserror::Error;
use crate::{
analysis::types::{LintT, LintWarning},
codemap::{CodeMap, FileSpan, Span},
syntax::{
ast::{Assign, AstAssignIdent, AstExpr, AstStmt, BinOp, Expr, Stmt},
AstModule,
},
};
#[derive(Error, Debug, VariantName)]
pub(crate) enum Incompatibility {
#[error("Type check `{0}` should be written `{1}`")]
IncompatibleTypeCheck(String, String),
#[error("Duplicate top-level assignment of `{0}`, first defined at {1}")]
DuplicateTopLevelAssign(String, FileSpan),
}
impl LintWarning for Incompatibility {
fn is_serious(&self) -> bool {
true
}
}
static TYPES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
hashmap![
"bool" => "True",
"tuple" => "()",
"str" => "\"\"",
"list" => "[]",
"int" => "0"
]
});
fn match_bad_type_equality(
codemap: &CodeMap,
x: &AstExpr,
types: &HashMap<&str, &str>,
res: &mut Vec<LintT<Incompatibility>>,
) {
fn lookup_type<'a>(x: &AstExpr, types: &HashMap<&str, &'a str>) -> Option<&'a str> {
match &**x {
Expr::Identifier(name, _) => types.get(name.node.as_str()).copied(),
_ => None,
}
}
fn is_type_call(x: &AstExpr) -> bool {
match &**x {
Expr::Call(fun, args) if args.len() == 1 => match &***fun {
Expr::Identifier(x, _) => x.node == "type",
_ => false,
},
_ => false,
}
}
match &**x {
Expr::Op(lhs, op, rhs)
if (*op == BinOp::Equal || *op == BinOp::NotEqual) && is_type_call(lhs) =>
{
if let Some(replacement) = lookup_type(rhs, types) {
res.push(LintT::new(
codemap,
x.span,
Incompatibility::IncompatibleTypeCheck(
x.to_string(),
format!("{}{}type({})", lhs.node, op, replacement),
),
))
}
}
_ => {}
}
}
fn bad_type_equality(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
let types = Lazy::force(&TYPES);
fn check(
codemap: &CodeMap,
x: &AstExpr,
types: &HashMap<&str, &str>,
res: &mut Vec<LintT<Incompatibility>>,
) {
match_bad_type_equality(codemap, x, types, res);
x.visit_expr(|x| check(codemap, x, types, res));
}
module
.statement
.visit_expr(|x| check(&module.codemap, x, types, res));
}
fn duplicate_top_level_assignment(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
let mut defined = HashMap::new(); let mut exported = HashSet::new();
fn ident<'a>(
x: &'a AstAssignIdent,
is_load: bool,
codemap: &CodeMap,
defined: &mut HashMap<&'a str, (Span, bool)>,
res: &mut Vec<LintT<Incompatibility>>,
) {
if let Some((old, _)) = defined.get(x.0.as_str()) {
res.push(LintT::new(
codemap,
x.span,
Incompatibility::DuplicateTopLevelAssign(x.0.clone(), codemap.file_span(*old)),
));
} else {
defined.insert(&x.0, (x.span, is_load));
}
}
fn stmt<'a>(
x: &'a AstStmt,
codemap: &CodeMap,
defined: &mut HashMap<&'a str, (Span, bool)>,
exported: &mut HashSet<&'a str>,
res: &mut Vec<LintT<Incompatibility>>,
) {
match &**x {
Stmt::Assign(lhs, rhs) => match (&**lhs, &***rhs) {
(Assign::Identifier(x), Expr::Identifier(y, _))
if x.node.0 == y.node
&& defined.get(x.node.0.as_str()).map_or(false, |x| x.1)
&& !exported.contains(x.node.0.as_str()) =>
{
exported.insert(x.node.0.as_str());
}
_ => lhs.visit_lvalue(|x| ident(x, false, codemap, defined, res)),
},
Stmt::AssignModify(lhs, _, _) => {
lhs.visit_lvalue(|x| ident(x, false, codemap, defined, res))
}
Stmt::Def(name, ..) => ident(name, false, codemap, defined, res),
Stmt::Load(load) => {
for (name, _) in &load.args {
ident(name, true, codemap, defined, res)
}
}
_ => x.visit_stmt(|x| stmt(x, codemap, defined, exported, res)),
}
}
stmt(
&module.statement,
&module.codemap,
&mut defined,
&mut exported,
res,
)
}
pub(crate) fn incompatibilities(module: &AstModule) -> Vec<LintT<Incompatibility>> {
let mut res = Vec::new();
bad_type_equality(module, &mut res);
duplicate_top_level_assignment(module, &mut res);
res
}
#[cfg(test)]
mod tests {
use gazebo::prelude::*;
use super::*;
use crate::syntax::Dialect;
fn module(x: &str) -> AstModule {
AstModule::parse("bad.py", x.to_owned(), &Dialect::Extended).unwrap()
}
#[test]
fn test_lint_incompatible() {
let mut res = Vec::new();
bad_type_equality(
&module(
r#"
def foo():
if type(x) == str and type(y) == type(list) and type(z) == foobar:
pass
"#,
),
&mut res,
);
assert_eq!(
res.map(|x| x.to_string()),
&[
"bad.py:3:8-22: Type check `(type(x) == str)` should be written `type(x) == type(\"\")`"
]
);
}
#[test]
fn test_lint_duplicate_top_level_assign() {
let m = module(
r#"
load("file", "foo", "no3", "no4")
no1 = 1
no1 = 4
no1 += 8
foo = foo # Starlark reexport
no3 = no3
no3 = no3
no4 = no4 + 1
def no2(): pass
def no2():
x = 1
x += 1
return x
"#,
);
let mut res = Vec::new();
duplicate_top_level_assignment(&m, &mut res);
let mut res = res.map(|x| match &x.problem {
Incompatibility::DuplicateTopLevelAssign(x, _) => x,
_ => panic!("Unexpected lint"),
});
res.sort();
assert_eq!(res, &["no1", "no1", "no2", "no3", "no4"])
}
}