starlark/analysis/
flow.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 starlark_syntax::syntax::ast::AstExpr;
19use starlark_syntax::syntax::ast::AstLiteral;
20use starlark_syntax::syntax::ast::AstStmt;
21use starlark_syntax::syntax::ast::AstTypeExpr;
22use starlark_syntax::syntax::ast::DefP;
23use starlark_syntax::syntax::ast::Expr;
24use starlark_syntax::syntax::ast::ForP;
25use starlark_syntax::syntax::ast::Stmt;
26use starlark_syntax::syntax::module::AstModuleFields;
27use thiserror::Error;
28
29use crate::analysis::types::LintT;
30use crate::analysis::types::LintWarning;
31use crate::analysis::EvalSeverity;
32use crate::codemap::CodeMap;
33use crate::codemap::ResolvedFileSpan;
34use crate::codemap::Span;
35use crate::codemap::Spanned;
36use crate::syntax::AstModule;
37
38#[derive(Error, Debug)]
39pub(crate) enum FlowIssue {
40    #[error("`return` lacks expression, but function `{0}` at {1} seems to want one due to {2}")]
41    MissingReturnExpression(String, ResolvedFileSpan, ResolvedFileSpan),
42    #[error("No `return` at the end, but function `{0}` seems to want one due to {1}")]
43    MissingReturn(String, ResolvedFileSpan),
44    #[error("Unreachable statement `{0}`")]
45    Unreachable(String),
46    #[error("Redundant `return` at the end of a function")]
47    RedundantReturn,
48    #[error("Redundant `continue` at the end of a loop")]
49    RedundantContinue,
50    #[error("A `load` statement not at the top of the file")]
51    MisplacedLoad,
52    #[error("Statement at has no effect")]
53    NoEffect,
54}
55
56impl LintWarning for FlowIssue {
57    fn severity(&self) -> EvalSeverity {
58        match self {
59            // Sometimes people add these to make flow clearer
60            FlowIssue::RedundantContinue | FlowIssue::RedundantReturn => EvalSeverity::Disabled,
61            _ => EvalSeverity::Warning,
62        }
63    }
64
65    fn short_name(&self) -> &'static str {
66        match self {
67            FlowIssue::MissingReturnExpression(..) => "missing-return-expression",
68            FlowIssue::MissingReturn(..) => "missing-return",
69            FlowIssue::Unreachable(..) => "unreachable",
70            FlowIssue::RedundantReturn => "redundant-return",
71            FlowIssue::RedundantContinue => "redundant-continue",
72            FlowIssue::MisplacedLoad => "misplaced-load",
73            FlowIssue::NoEffect => "no-effect",
74        }
75    }
76}
77
78fn returns(x: &AstStmt) -> Vec<(Span, Option<&AstExpr>)> {
79    fn f<'a>(x: &'a AstStmt, res: &mut Vec<(Span, Option<&'a AstExpr>)>) {
80        match &**x {
81            Stmt::Return(ret) => res.push((x.span, ret.as_ref())),
82            Stmt::Def(..) => {} // Do not descend
83            _ => x.visit_stmt(|x| f(x, res)),
84        }
85    }
86
87    let mut res = Vec::new();
88    f(x, &mut res);
89    res
90}
91
92// fail is kind of like a return with error
93fn is_fail(x: &AstExpr) -> bool {
94    match &**x {
95        Expr::Call(x, _) => match &***x {
96            Expr::Identifier(name) => name.node.ident == "fail",
97            _ => false,
98        },
99        _ => false,
100    }
101}
102
103fn has_effect(x: &AstExpr) -> bool {
104    match &**x {
105        Expr::Literal(x) => {
106            // String literals have the "effect" of providing documentation
107            matches!(x, AstLiteral::String(_))
108        }
109        Expr::Lambda(_) => false,
110        Expr::If(_) | Expr::Tuple(_) | Expr::List(_) | Expr::Dict(_) => {
111            let mut res = false;
112            x.visit_expr(|x| res = res || has_effect(x));
113            res
114        }
115        _ => true,
116    }
117}
118
119fn final_return(x: &AstStmt) -> bool {
120    match &**x {
121        Stmt::Return(_) => true,
122        Stmt::Expression(x) if is_fail(x) => true,
123        Stmt::Statements(xs) => match xs.last() {
124            None => false,
125            Some(x) => final_return(x),
126        },
127        Stmt::IfElse(_, x_y) => {
128            let (x, y) = &**x_y;
129            final_return(x) && final_return(y)
130        }
131        _ => false,
132    }
133}
134
135fn require_return_expression(ret_type: &Option<Box<AstTypeExpr>>) -> Option<Span> {
136    match ret_type {
137        None => None,
138        Some(x) => match &x.node.expr.node {
139            Expr::Identifier(x) if x.node.ident == "None" => None,
140            _ => Some(x.span),
141        },
142    }
143}
144
145fn check_stmt(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
146    match &**x {
147        Stmt::Def(DefP {
148            name,
149            params: _,
150            return_type,
151            body,
152            payload: _,
153        }) => {
154            let rets = returns(body);
155
156            // Do I require my return statements to have an expression
157            let require_expression = require_return_expression(return_type)
158                .or_else(|| rets.iter().find(|x| x.1.is_some()).map(|x| x.0));
159            if let Some(reason) = require_expression {
160                if !final_return(body) {
161                    res.push(LintT::new(
162                        codemap,
163                        x.span,
164                        FlowIssue::MissingReturn(
165                            // Statements often end with \n, so remove that to fit nicely
166                            name.node.ident.trim_end().to_owned(),
167                            codemap.file_span(reason).resolve(),
168                        ),
169                    ));
170                }
171                for (span, ret) in rets {
172                    if ret.is_none() {
173                        res.push(LintT::new(
174                            codemap,
175                            span,
176                            FlowIssue::MissingReturnExpression(
177                                name.ident.clone(),
178                                codemap.file_span(x.span).resolve(),
179                                codemap.file_span(reason).resolve(),
180                            ),
181                        ))
182                    }
183                }
184            }
185        }
186        _ => {}
187    }
188}
189
190fn stmt(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
191    check_stmt(codemap, x, res);
192    x.visit_stmt(|x| stmt(codemap, x, res));
193}
194
195// Returns true if the code aborts this sequence early, due to return, fail, break or continue
196fn reachable(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) -> bool {
197    match &**x {
198        Stmt::Break | Stmt::Continue | Stmt::Return(_) => true,
199        Stmt::Expression(x) => is_fail(x),
200        Stmt::Statements(xs) => {
201            let mut i = xs.iter();
202            while let Some(x) = i.next() {
203                let aborts = reachable(codemap, x, res);
204                if aborts {
205                    if let Some(nxt) = i.next() {
206                        res.push(LintT::new(
207                            codemap,
208                            nxt.span,
209                            FlowIssue::Unreachable(nxt.node.to_string().trim().to_owned()),
210                        ))
211                    }
212                    // All the remaining statements are totally unreachable, but we declared that once
213                    // so don't even bother looking at them
214                    return aborts;
215                }
216            }
217            false
218        }
219        Stmt::IfElse(_, x_y) => {
220            let (x, y) = &**x_y;
221            let abort1 = reachable(codemap, x, res);
222            let abort2 = reachable(codemap, y, res);
223            abort1 && abort2
224        }
225        // For all remaining constructs, visit their children to accumulate errors,
226        // but even if they are present with returns, you don't guarantee the code with inner returns
227        // gets executed.
228        _ => {
229            x.visit_stmt(|x| {
230                reachable(codemap, x, res);
231            });
232            false
233        }
234    }
235}
236
237// If you have a definition which ends with return, or a loop which ends with continue
238// that is a useless statement that just
239fn redundant(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
240    fn check(is_loop: bool, codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
241        match &**x {
242            Stmt::Continue if is_loop => {
243                res.push(LintT::new(codemap, x.span, FlowIssue::RedundantContinue))
244            }
245            Stmt::Return(None) if !is_loop => {
246                res.push(LintT::new(codemap, x.span, FlowIssue::RedundantReturn))
247            }
248            Stmt::Statements(xs) if !xs.is_empty() => {
249                check(is_loop, codemap, xs.last().unwrap(), res)
250            }
251            Stmt::If(_, x) => check(is_loop, codemap, x, res),
252            Stmt::IfElse(_, x_y) => {
253                let (x, y) = &**x_y;
254                check(is_loop, codemap, x, res);
255                check(is_loop, codemap, y, res);
256            }
257            _ => {}
258        }
259    }
260
261    fn f(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
262        match &**x {
263            Stmt::For(ForP { body, .. }) => check(true, codemap, body, res),
264            Stmt::Def(DefP { body, .. }) => check(false, codemap, body, res),
265            _ => {}
266        }
267        // We always want to look inside everything for other types of violation
268        x.visit_stmt(|x| f(codemap, x, res))
269    }
270
271    x.visit_stmt(|x| f(codemap, x, res));
272}
273
274fn misplaced_load(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
275    // accumulate all statements at the top-level
276    fn top_statements<'a>(x: &'a AstStmt, stmts: &mut Vec<&'a AstStmt>) {
277        match &**x {
278            Stmt::Statements(xs) => {
279                for x in xs {
280                    top_statements(x, stmts);
281                }
282            }
283            _ => stmts.push(x),
284        }
285    }
286
287    let mut stmts = Vec::new();
288    top_statements(x, &mut stmts);
289
290    // We allow loads or documentation strings, but after that, no loads
291    let mut allow_loads = true;
292    for x in stmts {
293        match &**x {
294            Stmt::Load(..) => {
295                if !allow_loads {
296                    res.push(LintT::new(codemap, x.span, FlowIssue::MisplacedLoad))
297                }
298            }
299            Stmt::Expression(Spanned {
300                node: Expr::Literal(AstLiteral::String(_)),
301                ..
302            }) => {
303                // Still allow loads after a literal string (probably documentation)
304            }
305            _ => allow_loads = false,
306        }
307    }
308}
309
310fn no_effect(codemap: &CodeMap, x: &AstStmt, res: &mut Vec<LintT<FlowIssue>>) {
311    match &**x {
312        Stmt::Expression(x) if !has_effect(x) => {
313            res.push(LintT::new(codemap, x.span, FlowIssue::NoEffect))
314        }
315        _ => x.visit_stmt(|x| no_effect(codemap, x, res)),
316    }
317}
318
319pub(crate) fn lint(module: &AstModule) -> Vec<LintT<FlowIssue>> {
320    let mut res = Vec::new();
321    stmt(module.codemap(), module.statement(), &mut res);
322    reachable(module.codemap(), module.statement(), &mut res);
323    redundant(module.codemap(), module.statement(), &mut res);
324    misplaced_load(module.codemap(), module.statement(), &mut res);
325    no_effect(module.codemap(), module.statement(), &mut res);
326    res
327}
328
329#[cfg(test)]
330mod tests {
331    use starlark_syntax::slice_vec_ext::SliceExt;
332
333    use super::*;
334    use crate::syntax::Dialect;
335
336    fn module(x: &str) -> AstModule {
337        AstModule::parse("X", x.to_owned(), &Dialect::AllOptionsInternal).unwrap()
338    }
339
340    impl FlowIssue {
341        fn about(&self) -> &String {
342            match self {
343                FlowIssue::MissingReturnExpression(x, _, _) => x,
344                FlowIssue::MissingReturn(x, _) => x,
345                FlowIssue::Unreachable(x) => x,
346                _ => panic!("Should not be used on such issues in test code"),
347            }
348        }
349    }
350
351    #[test]
352    fn test_lint_returns() {
353        let m = module(
354            r#"
355def no1() -> str:
356    pass
357def no2():
358    if x:
359        return 1
360def no3():
361    if x:
362        return
363    return 1
364def ok():
365    def no4() -> int:
366        no4()
367    pass
368def yes1():
369    pass
370def yes2() -> str:
371    yes1()
372    if x:
373        return "x"
374    else:
375        return "y"
376def yes3():
377    if x:
378        return
379    pass
380def yes4() -> str:
381    fail("die")
382"#,
383        );
384        let mut res = Vec::new();
385        stmt(m.codemap(), m.statement(), &mut res);
386        assert_eq!(
387            res.map(|x| x.problem.about()),
388            &["no1", "no2", "no3", "no4"]
389        );
390    }
391
392    #[test]
393    fn test_lint_unreachable() {
394        let m = module(
395            r#"
396def test():
397    return 1
398    no1
399def test2():
400    if x:
401        return 1
402    yes
403def test3():
404    if x:
405        return
406    else:
407        return
408    no2
409    ignored
410def test4():
411    for x in xs:
412        continue
413        no3
414    reachable
415def test5():
416    for x in xs:
417        if test:
418            continue
419        reachable
420        return
421    reachable
422def test6():
423    fail(1)
424    no4
425def f():
426    def g():
427        return 5
428    reachable
429"#,
430        );
431        let mut res = Vec::new();
432        reachable(m.codemap(), m.statement(), &mut res);
433        assert_eq!(
434            res.map(|x| x.problem.about()),
435            &["no1", "no2", "no3", "no4"]
436        );
437    }
438
439    #[test]
440    fn test_lint_redundant() {
441        let m = module(
442            r#"
443def test(): # 1
444    foo
445    return # bad: 3
446def test2(): # 4
447    return
448    foo
449def test3(): # 7
450    if x:
451        return # bad: 9
452    else:
453        y + 1
454def test4(): # 12
455    def test5():
456        for x in xs:
457            test
458            if x:
459                return
460            else:
461                continue # bad: 19
462    test5()
463def test6():
464    if x:
465        return
466    y + 1
467def test7():
468    for x in xs:
469        if x:
470            continue
471        return
472"#,
473        );
474        let mut res = Vec::new();
475        redundant(m.codemap(), m.statement(), &mut res);
476        assert_eq!(
477            res.map(|x| x.location.resolve_span().begin.line),
478            &[3, 9, 19]
479        );
480    }
481
482    #[test]
483    fn test_lint_misplaced_load() {
484        let m = module(
485            r#"
486load("a", "a")
487"""
488this is some comment
489over multiple lines
490"""
491load("b", "b")
492
493x = 1
494load("c", "b")
495"#,
496        );
497        let mut res = Vec::new();
498        misplaced_load(m.codemap(), m.statement(), &mut res);
499        assert_eq!(res.len(), 1);
500    }
501
502    #[test]
503    fn test_lint_no_effect() {
504        let src = r#"
505"""
506a doc block
507"""
508load("b", "b")
509
510x = 1
5117 ## BAD
512def foo():
513    [18] ## BAD
5141 + 2
515"#;
516
517        let m = module(src);
518        let bad = src
519            .lines()
520            .enumerate()
521            .filter_map(|(i, x)| x.contains("## BAD").then_some(i))
522            .collect::<Vec<_>>();
523        let mut res = Vec::new();
524        no_effect(m.codemap(), m.statement(), &mut res);
525        assert_eq!(res.map(|x| x.location.resolve_span().begin.line), bad);
526    }
527}