rslint_core/groups/errors/no_unsafe_finally.rs
1use crate::rule_prelude::*;
2use SyntaxKind::*;
3
4declare_lint! {
5 /**
6 Forbid the use of unsafe control flow statements in try and catch blocks.
7
8 JavaScript suspends any running control flow statements inside of `try` and `catch` blocks until
9 `finally` is done executing. This means that any control statements such as `return`, `throw`, `break`,
10 and `continue` which are used inside of a `finally` will override any control statements in `try` and `catch`.
11 This is almost always unexpected behavior.
12
13 ## Incorrect Code Examples
14
15 ```js
16 // We expect 10 to be returned, but 5 is actually returned
17 function foo() {
18 try {
19 return 10;
20 // ^^^^^^^^^ this statement is executed, but actually returning is paused...
21 } finally {
22 return 5;
23 // ^^^^^^^^^ ...finally is executed, and this statement returns from the function, **the previous is ignored**
24 }
25 }
26 foo() // 5
27 ```
28
29 Throwing errors inside try statements
30
31 ```js
32 // We expect an error to be thrown, then 5 to be returned, but the error is not thrown
33 function foo() {
34 try {
35 throw new Error("bar");
36 // ^^^^^^^^^^^^^^^^^^^^^^^ this statement is executed but throwing the error is paused...
37 } finally {
38 return 5;
39 // ^^^^^^^^^ ...we expect the error to be thrown and then for 5 to be returned,
40 // but 5 is returned early, **the error is not thrown**.
41 }
42 }
43 foo() // 5
44 ```
45 */
46 #[derive(Default)]
47 NoUnsafeFinally,
48 errors,
49 tags(Recommended),
50 "no-unsafe-finally"
51}
52
53pub const CONTROL_FLOW_STMT: [SyntaxKind; 4] = [BREAK_STMT, CONTINUE_STMT, THROW_STMT, RETURN_STMT];
54
55#[typetag::serde]
56impl CstRule for NoUnsafeFinally {
57 fn check_node(&self, node: &SyntaxNode, ctx: &mut RuleCtx) -> Option<()> {
58 if CONTROL_FLOW_STMT.contains(&node.kind())
59 && node.parent()?.parent()?.is::<ast::Finalizer>()
60 {
61 self.output(node, ctx);
62 }
63 None
64 }
65}
66
67impl NoUnsafeFinally {
68 fn output(&self, node: &SyntaxNode, ctx: &mut RuleCtx) -> Option<()> {
69 let parent = if node.parent()?.kind() == FINALIZER {
70 node.parent()?
71 } else {
72 node.parent()?.parent()?
73 };
74
75 let try_stmt = parent.parent()?.to::<ast::TryStmt>();
76
77 let err = if let Some(control) = try_stmt
78 .test()?
79 .syntax()
80 .children()
81 .find(|it| CONTROL_FLOW_STMT.contains(&it.kind()))
82 {
83 let err = ctx.err(
84 self.name(),
85 format!(
86 "Unsafe usage of a {} inside of a Try statement",
87 node.readable_stmt_name()
88 ),
89 );
90
91 let get_kind = |kind: SyntaxKind| match kind {
92 RETURN_STMT => "returning from the block",
93 CONTINUE_STMT => "continuing the loop",
94 THROW_STMT => "throwing this error",
95 BREAK_STMT => "breaking from the loop",
96 _ => unreachable!(),
97 };
98
99 err.secondary(
100 control.clone(),
101 format!(
102 "{} is paused until the `finally` block is done executing...",
103 get_kind(control.kind())
104 ),
105 )
106 .primary(
107 node,
108 format!(
109 "...however, {} exits the statement altogether",
110 get_kind(node.kind())
111 ),
112 )
113 .primary(
114 node,
115 format!("which makes `{}` never finish running", control),
116 )
117 } else {
118 ctx.err(
119 self.name(),
120 format!(
121 "Unsafe usage of a {} inside of a Try statement",
122 node.readable_stmt_name()
123 ),
124 )
125 .primary(
126 node,
127 "this statement abruptly ends execution, yielding unwanted behavior",
128 )
129 };
130
131 ctx.add_err(err);
132 None
133 }
134}
135
136rule_tests! {
137 NoUnsafeFinally::default(),
138 err: {
139 "
140 try {
141 throw A;
142 } finally {
143 return;
144 }
145 ",
146 "
147 try {
148 throw new Error();
149 } catch {
150
151 } finally {
152 continue;
153 }
154 ",
155 /// ignore
156 "
157 try {
158 {}
159 } finally {
160 try {} finally {
161 return 5;
162 }
163 }
164 "
165 },
166 ok: {
167 "
168 try {
169 throw A;
170 } finally {
171 if (false) {
172 return true;
173 }
174 }
175 "
176 }
177}