starlark/analysis/
incompatible.rs

1/*
2 * Copyright 2019 The Starlark in Rust Authors.
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18use 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    // Return true if this expression matches `type($x)`
88    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    // If we see type(x) == y (or negated), where y is in our types table, suggest a replacement
99    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
134// Go implementation of Starlark disallows duplicate top-level assignments,
135// it's likely that will become Starlark standard sooner or later, so check now.
136// The one place we allow it is to export something you grabbed with load.
137fn duplicate_top_level_assignment(module: &AstModule, res: &mut Vec<LintT<Incompatibility>>) {
138    let mut defined = HashMap::new(); //(name, (location, is_load))
139    let mut exported = HashSet::new(); // name's already exported by is_load
140
141    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                    // Normally this would be an error, but if we load()'d it, this is how we'd reexport through Starlark.
174                    // But only allow one export
175                    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            // Visit statements, but don't descend under def - only top-level statements are interesting
191            _ => 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}