1use std::collections::HashMap;
19use std::collections::HashSet;
20
21use maplit::hashmap;
22use once_cell::sync::Lazy;
23use starlark_syntax::syntax::ast::AssignTarget;
24use starlark_syntax::syntax::ast::AstAssignIdent;
25use starlark_syntax::syntax::ast::AstExpr;
26use starlark_syntax::syntax::ast::AstStmt;
27use starlark_syntax::syntax::ast::BinOp;
28use starlark_syntax::syntax::ast::DefP;
29use starlark_syntax::syntax::ast::Expr;
30use starlark_syntax::syntax::ast::LoadArgP;
31use starlark_syntax::syntax::ast::Stmt;
32use starlark_syntax::syntax::module::AstModuleFields;
33use thiserror::Error;
34
35use crate::analysis::types::LintT;
36use crate::analysis::types::LintWarning;
37use crate::analysis::EvalSeverity;
38use crate::codemap::CodeMap;
39use crate::codemap::FileSpan;
40use crate::codemap::Span;
41use crate::syntax::AstModule;
42
43#[derive(Error, Debug)]
44pub(crate) enum Incompatibility {
45 #[error("Type check `{0}` should be written `{1}`")]
46 IncompatibleTypeCheck(String, String),
47 #[error("Duplicate top-level assignment of `{0}`, first defined at {1}")]
48 DuplicateTopLevelAssign(String, FileSpan),
49}
50
51impl LintWarning for Incompatibility {
52 fn severity(&self) -> EvalSeverity {
53 EvalSeverity::Warning
54 }
55
56 fn short_name(&self) -> &'static str {
57 match self {
58 Incompatibility::IncompatibleTypeCheck(..) => "incompatible-type-check",
59 Incompatibility::DuplicateTopLevelAssign(..) => "duplicate-top-level-assign",
60 }
61 }
62}
63
64static TYPES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
65 hashmap![
66 "bool" => "True",
67 "tuple" => "()",
68 "str" => "\"\"",
69 "list" => "[]",
70 "int" => "0"
71 ]
72});
73
74fn match_bad_type_equality(
75 codemap: &CodeMap,
76 x: &AstExpr,
77 types: &HashMap<&str, &str>,
78 res: &mut Vec<LintT<Incompatibility>>,
79) {
80 fn lookup_type<'a>(x: &AstExpr, types: &HashMap<&str, &'a str>) -> Option<&'a str> {
81 match &**x {
82 Expr::Identifier(name) => types.get(name.node.ident.as_str()).copied(),
83 _ => None,
84 }
85 }
86
87 fn is_type_call(x: &AstExpr) -> bool {
89 match &**x {
90 Expr::Call(fun, args) if args.args.len() == 1 => match &***fun {
91 Expr::Identifier(x) => x.node.ident == "type",
92 _ => false,
93 },
94 _ => false,
95 }
96 }
97
98 match &**x {
100 Expr::Op(lhs, op, rhs)
101 if (*op == BinOp::Equal || *op == BinOp::NotEqual) && is_type_call(lhs) =>
102 {
103 if let Some(replacement) = lookup_type(rhs, types) {
104 res.push(LintT::new(
105 codemap,
106 x.span,
107 Incompatibility::IncompatibleTypeCheck(
108 x.to_string(),
109 format!("{}{}type({})", lhs.node, op, replacement),
110 ),
111 ))
112 }
113 }
114 _ => {}
115 }
116}
117
118fn bad_type_equality(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
119 let types = Lazy::force(&TYPES);
120 fn check(
121 codemap: &CodeMap,
122 x: &AstExpr,
123 types: &HashMap<&str, &str>,
124 res: &mut Vec<LintT<Incompatibility>>,
125 ) {
126 match_bad_type_equality(codemap, x, types, res);
127 x.visit_expr(|x| check(codemap, x, types, res));
128 }
129 module
130 .statement()
131 .visit_expr(|x| check(module.codemap(), x, types, res));
132}
133
134fn duplicate_top_level_assignment(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
138 let mut defined = HashMap::new(); let mut exported = HashSet::new(); fn ident<'a>(
142 x: &'a AstAssignIdent,
143 is_load: bool,
144 codemap: &CodeMap,
145 defined: &mut HashMap<&'a str, (Span, bool)>,
146 res: &mut Vec<LintT<Incompatibility>>,
147 ) {
148 if let Some((old, _)) = defined.get(x.ident.as_str()) {
149 res.push(LintT::new(
150 codemap,
151 x.span,
152 Incompatibility::DuplicateTopLevelAssign(x.ident.clone(), codemap.file_span(*old)),
153 ));
154 } else {
155 defined.insert(&x.ident, (x.span, is_load));
156 }
157 }
158
159 fn stmt<'a>(
160 x: &'a AstStmt,
161 codemap: &CodeMap,
162 defined: &mut HashMap<&'a str, (Span, bool)>,
163 exported: &mut HashSet<&'a str>,
164 res: &mut Vec<LintT<Incompatibility>>,
165 ) {
166 match &**x {
167 Stmt::Assign(assign) => match (&assign.lhs.node, &assign.rhs.node) {
168 (AssignTarget::Identifier(x), Expr::Identifier(y))
169 if x.node.ident == y.node.ident
170 && defined.get(x.node.ident.as_str()).map_or(false, |x| x.1)
171 && !exported.contains(x.node.ident.as_str()) =>
172 {
173 exported.insert(x.node.ident.as_str());
176 }
177 _ => assign
178 .lhs
179 .visit_lvalue(|x| ident(x, false, codemap, defined, res)),
180 },
181 Stmt::AssignModify(lhs, _, _) => {
182 lhs.visit_lvalue(|x| ident(x, false, codemap, defined, res))
183 }
184 Stmt::Def(DefP { name, .. }) => ident(name, false, codemap, defined, res),
185 Stmt::Load(load) => {
186 for LoadArgP { local, .. } in &load.args {
187 ident(local, true, codemap, defined, res)
188 }
189 }
190 _ => x.visit_stmt(|x| stmt(x, codemap, defined, exported, res)),
192 }
193 }
194
195 stmt(
196 module.statement(),
197 module.codemap(),
198 &mut defined,
199 &mut exported,
200 res,
201 )
202}
203
204pub(crate) fn lint(module: &AstModule) -> Vec<LintT<Incompatibility>> {
205 let mut res = Vec::new();
206 bad_type_equality(module, &mut res);
207 duplicate_top_level_assignment(module, &mut res);
208 res
209}
210
211#[cfg(test)]
212mod tests {
213 use starlark_syntax::slice_vec_ext::SliceExt;
214
215 use super::*;
216 use crate::syntax::Dialect;
217
218 fn module(x: &str) -> AstModule {
219 AstModule::parse("bad.py", x.to_owned(), &Dialect::AllOptionsInternal).unwrap()
220 }
221
222 #[test]
223 fn test_lint_incompatible() {
224 let mut res = Vec::new();
225 bad_type_equality(
226 &module(
227 r#"
228def foo():
229 if type(x) == str and type(y) == type(list) and type(z) == foobar:
230 pass
231"#,
232 ),
233 &mut res,
234 );
235 assert_eq!(
236 res.map(|x| x.to_string()),
237 &[
238 "bad.py:3:8-22: Type check `(type(x) == str)` should be written `type(x) == type(\"\")`"
239 ]
240 );
241 }
242
243 #[test]
244 fn test_lint_duplicate_top_level_assign() {
245 let m = module(
246 r#"
247load("file", "foo", "no3", "no4")
248no1 = 1
249no1 = 4
250no1 += 8
251foo = foo # Starlark reexport
252no3 = no3
253no3 = no3
254no4 = no4 + 1
255def no2(): pass
256def no2():
257 x = 1
258 x += 1
259 return x
260"#,
261 );
262 let mut res = Vec::new();
263 duplicate_top_level_assignment(&m, &mut res);
264 let mut res = res.map(|x| match &x.problem {
265 Incompatibility::DuplicateTopLevelAssign(x, _) => x,
266 _ => panic!("Unexpected lint"),
267 });
268 res.sort();
269 assert_eq!(res, &["no1", "no1", "no2", "no3", "no4"])
270 }
271}