1use 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 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(..) => {} _ => 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
92fn 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 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 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 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
195fn 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 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 _ => {
229 x.visit_stmt(|x| {
230 reachable(codemap, x, res);
231 });
232 false
233 }
234 }
235}
236
237fn 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 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 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 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 }
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}