mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
319 lines
12 KiB
Rust
319 lines
12 KiB
Rust
use oxc_ast::{ast::VariableDeclarationKind, AstKind};
|
|
use oxc_diagnostics::OxcDiagnostic;
|
|
use oxc_macros::declare_oxc_lint;
|
|
use oxc_semantic::{
|
|
control_flow::graph::{
|
|
visit::{depth_first_search, Control, DfsEvent, EdgeRef},
|
|
Direction,
|
|
},
|
|
EdgeType, ErrorEdgeKind, InstructionKind,
|
|
};
|
|
use oxc_span::{GetSpan, Span};
|
|
|
|
use crate::{context::LintContext, rule::Rule};
|
|
|
|
fn no_unreachable_diagnostic(span: Span) -> OxcDiagnostic {
|
|
OxcDiagnostic::error("eslint(no-unreachable): Unreachable code.").with_labels([span.into()])
|
|
}
|
|
|
|
/// <https://github.com/eslint/eslint/blob/069aa680c78b8516b9a1b568519f1d01e74fb2a2/lib/rules/no-unreachable.js#L196>
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct NoUnreachable;
|
|
|
|
declare_oxc_lint!(
|
|
/// ### What it does
|
|
///
|
|
/// Disallow unreachable code after `return`, `throw`, `continue`, and `break` statements
|
|
///
|
|
NoUnreachable,
|
|
nursery
|
|
);
|
|
|
|
impl Rule for NoUnreachable {
|
|
fn run_once(&self, ctx: &LintContext) {
|
|
let nodes = ctx.nodes();
|
|
let Some(root) = nodes.root_node() else { return };
|
|
let cfg = ctx.semantic().cfg();
|
|
let graph = &cfg.graph;
|
|
|
|
// A pre-allocated vector containing the reachability status of all the basic blocks.
|
|
// We initialize this vector with all nodes set to `unreachable` since if we don't visit a
|
|
// node in our paths then it should be unreachable by definition.
|
|
let mut unreachables = vec![true; cfg.basic_blocks.len()];
|
|
|
|
// All of the end points of infinite loops we encountered.
|
|
let mut infinite_loops = Vec::new();
|
|
|
|
// Set the root as reachable.
|
|
unreachables[root.cfg_id().index()] = false;
|
|
|
|
// In our first path we first check if each block is definitely unreachable, If it is then
|
|
// we set it as such, If we encounter an infinite loop we keep its end block since it can
|
|
// prevent other reachable blocks from ever getting executed.
|
|
let _: Control<()> = depth_first_search(graph, Some(root.cfg_id()), |event| {
|
|
if let DfsEvent::Finish(node, _) = event {
|
|
let unreachable = cfg.basic_block(node).unreachable;
|
|
unreachables[node.index()] = unreachable;
|
|
|
|
if !unreachable {
|
|
if let Some(it) = cfg.is_infinite_loop_start(node, nodes) {
|
|
infinite_loops.push(it);
|
|
}
|
|
}
|
|
}
|
|
Control::Continue
|
|
});
|
|
|
|
// In the second path we go for each infinite loop end block and follow it marking all
|
|
// edges as unreachable unless they have a reachable jump (eg. break).
|
|
for loop_ in infinite_loops {
|
|
// A loop end block usually is also its condition and start point but what is common
|
|
// in all cases is that it may have `Jump` or `Backedge` edges so we only want to
|
|
// follow the `Normal` edges as these are the exiting edges.
|
|
let starts: Vec<_> = graph
|
|
.edges_directed(loop_.1, Direction::Outgoing)
|
|
.filter(|it| matches!(it.weight(), EdgeType::Normal))
|
|
.map(|it| it.target())
|
|
.collect();
|
|
|
|
// Search with all `Normal` edges as starting point(s).
|
|
let _: Control<()> = depth_first_search(graph, starts, |event| match event {
|
|
DfsEvent::Discover(node, _) => {
|
|
let mut incoming = graph.edges_directed(node, Direction::Incoming);
|
|
if incoming.any(|e| match e.weight() {
|
|
// `NewFunction` is always reachable
|
|
| EdgeType::NewFunction
|
|
// `Finalize` can be reachable if we encounter an error in the loop.
|
|
| EdgeType::Finalize
|
|
// Explicit `Error` can also be reachable if we encounter an error in the loop.
|
|
| EdgeType::Error(ErrorEdgeKind::Explicit) => true,
|
|
|
|
// If we have an incoming `Jump` and it is from a `Break` instruction,
|
|
// We know with high confidence that we are visiting a reachable block.
|
|
// NOTE: May cause false negatives but I couldn't think of one.
|
|
EdgeType::Jump
|
|
if cfg
|
|
.basic_block(e.source())
|
|
.instructions()
|
|
.iter()
|
|
.any(|it| matches!(it.kind, InstructionKind::Break(_))) =>
|
|
{
|
|
true
|
|
}
|
|
_ => false,
|
|
}) {
|
|
// We prune this branch if it is reachable from this point forward.
|
|
Control::Prune
|
|
} else {
|
|
// Otherwise we set it to unreachable and continue.
|
|
unreachables[node.index()] = true;
|
|
Control::Continue
|
|
}
|
|
}
|
|
_ => Control::Continue,
|
|
});
|
|
}
|
|
for node in ctx.nodes().iter() {
|
|
// exit early if we are not visiting a statement.
|
|
if !node.kind().is_statement() {
|
|
continue;
|
|
}
|
|
|
|
// exit early if it is an empty statement.
|
|
if matches!(node.kind(), AstKind::EmptyStatement(_)) {
|
|
continue;
|
|
}
|
|
|
|
if matches! {
|
|
node.kind(),
|
|
AstKind::VariableDeclaration(decl)
|
|
if matches!(decl.kind, VariableDeclarationKind::Var) && !decl.has_init()
|
|
} {
|
|
// Skip `var` declarations without any initialization,
|
|
// These work because of the JavaScript hoisting rules.
|
|
continue;
|
|
}
|
|
|
|
if unreachables[node.cfg_id().index()] {
|
|
ctx.diagnostic(no_unreachable_diagnostic(node.kind().span()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test() {
|
|
use crate::tester::Tester;
|
|
|
|
let pass = vec![
|
|
"function foo() { function bar() { return 1; } return bar(); }",
|
|
"function foo() { return bar(); function bar() { return 1; } }",
|
|
"function foo() { return x; var x; }",
|
|
"function foo() { var x = 1; var y = 2; }",
|
|
"function foo() { var x = 1; var y = 2; return; }",
|
|
"while (true) { switch (foo) { case 1: x = 1; x = 2;} }",
|
|
"while (true) { break; var x; }",
|
|
"while (true) { continue; var x, y; }",
|
|
"while (true) { throw 'message'; var x; }",
|
|
"while (true) { if (true) break; var x = 1; }",
|
|
"while (true) continue;",
|
|
"switch (foo) { case 1: break; var x; }",
|
|
"switch (foo) { case 1: break; var x; default: throw true; };",
|
|
"const arrow_direction = arrow => { switch (arrow) { default: throw new Error(); };}",
|
|
"var x = 1; y = 2; throw 'uh oh'; var y;",
|
|
"function foo() { var x = 1; if (x) { return; } x = 2; }",
|
|
"function foo() { var x = 1; if (x) { } else { return; } x = 2; }",
|
|
"function foo() { var x = 1; switch (x) { case 0: break; default: return; } x = 2; }",
|
|
"function foo() { var x = 1; while (x) { return; } x = 2; }",
|
|
"function foo() { var x = 1; for (x in {}) { return; } x = 2; }",
|
|
"function foo() { var x = 1; try { return; } finally { x = 2; } }",
|
|
"function foo() { var x = 1; for (;;) { if (x) break; } x = 2; }",
|
|
"A: { break A; } foo()",
|
|
"function* foo() { try { yield 1; return; } catch (err) { return err; } }",
|
|
"function foo() { try { bar(); return; } catch (err) { return err; } }",
|
|
"function foo() { try { a.b.c = 1; return; } catch (err) { return err; } }",
|
|
"class C { foo = reachable; }",
|
|
"class C { foo = reachable; constructor() {} }",
|
|
"class C extends B { foo = reachable; }",
|
|
"class C extends B { foo = reachable; constructor() { super(); } }",
|
|
"class C extends B { static foo = reachable; constructor() {} }",
|
|
"function foo() { var x = 1; for (;x == 1;) { if (x) continue; } x = 2; }",
|
|
"
|
|
if (a) {
|
|
a();
|
|
} else {
|
|
for (let i = 1; i <= 10; i++) {
|
|
b();
|
|
}
|
|
|
|
for (let i = 1; i <= 10; i++) {
|
|
c();
|
|
}
|
|
}
|
|
",
|
|
"
|
|
try {
|
|
throw 'error';
|
|
} catch (err) {
|
|
b();
|
|
}
|
|
c();
|
|
",
|
|
"
|
|
export const getPagePreviewText = (page) => {
|
|
if (!a) {
|
|
return '';
|
|
}
|
|
while (a && b > c && d-- > 0) {
|
|
}
|
|
};
|
|
",
|
|
"
|
|
try {
|
|
for (const a of b) {
|
|
c();
|
|
}
|
|
|
|
while (true) {
|
|
d();
|
|
}
|
|
} finally {
|
|
}
|
|
",
|
|
"
|
|
switch (authType) {
|
|
case 1:
|
|
return a();
|
|
case 2:
|
|
return b();
|
|
case 3:
|
|
return c();
|
|
}
|
|
d();
|
|
",
|
|
"
|
|
try {
|
|
a();
|
|
} catch (e) {
|
|
b();
|
|
} finally {
|
|
c();
|
|
}
|
|
d();
|
|
",
|
|
"
|
|
try {
|
|
while (true) {
|
|
a();
|
|
}
|
|
} finally {
|
|
b();
|
|
}
|
|
",
|
|
"
|
|
try {
|
|
a();
|
|
} finally {
|
|
b();
|
|
}
|
|
c();
|
|
",
|
|
"
|
|
try {
|
|
while (true) {
|
|
a();
|
|
}
|
|
} catch {
|
|
b();
|
|
}
|
|
",
|
|
];
|
|
|
|
let fail = vec![
|
|
//[{ messageId: "unreachableCode", type: "VariableDeclaration" }]
|
|
"function foo() { return x; var x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "VariableDeclaration" }]
|
|
"function foo() { return x; var x, y = 1; }",
|
|
"while (true) { break; var x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "VariableDeclaration" }]
|
|
"while (true) { continue; var x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { return; x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { throw error; x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"while (true) { break; x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"while (true) { continue; x = 1; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { switch (foo) { case 1: return; x = 1; } }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { switch (foo) { case 1: throw e; x = 1; } }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"while (true) { switch (foo) { case 1: break; x = 1; } }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"while (true) { switch (foo) { case 1: continue; x = 1; } }",
|
|
//[{ messageId: "unreachableCode", type: "VariableDeclaration" }]
|
|
"var x = 1; throw 'uh oh'; var y = 2;",
|
|
// [{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; if (x) { return; } else { throw e; } x = 2; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; if (x) return; else throw -1; x = 2; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; try { return; } finally {} x = 2; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; try { } finally { return; } x = 2; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; do { return; } while (x); x = 2; }",
|
|
// [{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; while (x) { if (x) break; else continue; x = 2; } }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; for (;;) { if (x) continue; } x = 2; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; while (true) { } x = 2; }",
|
|
//[{ messageId: "unreachableCode", type: "ExpressionStatement" }]
|
|
"function foo() { var x = 1; do { } while (true); x = 2; }",
|
|
];
|
|
|
|
Tester::new(NoUnreachable::NAME, pass, fail).test_and_snapshot();
|
|
}
|