From 0b9e122de8687dfb20204927c893e2c5b8c19c3c Mon Sep 17 00:00:00 2001 From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:37:22 +0900 Subject: [PATCH] feat(linter): Implement `unicorn/no-process-exit` rule (#2410) Part of #684 - docs: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-process-exit.md - source: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/rules/no-process-exit.js - tests: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/test/no-process-exit.mjs --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/unicorn/no_process_exit.rs | 187 ++++++++++++++++++ .../src/snapshots/no_process_exit.snap | 153 ++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 crates/oxc_linter/src/rules/unicorn/no_process_exit.rs create mode 100644 crates/oxc_linter/src/snapshots/no_process_exit.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b9a3bb180..3874e2025 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -203,6 +203,7 @@ mod unicorn { pub mod no_new_buffer; pub mod no_null; pub mod no_object_as_default_parameter; + pub mod no_process_exit; pub mod no_static_only_class; pub mod no_thenable; pub mod no_this_assignment; @@ -463,6 +464,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::no_new_buffer, unicorn::no_null, unicorn::no_object_as_default_parameter, + unicorn::no_process_exit, unicorn::no_static_only_class, unicorn::no_thenable, unicorn::no_this_assignment, diff --git a/crates/oxc_linter/src/rules/unicorn/no_process_exit.rs b/crates/oxc_linter/src/rules/unicorn/no_process_exit.rs new file mode 100644 index 000000000..5641197fd --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/no_process_exit.rs @@ -0,0 +1,187 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ast_util::is_method_call, context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`.")] +#[diagnostic( + severity(warning), + help("Only use `process.exit()` in CLI apps. Throw an error instead.") +)] +struct NoProcessExitDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoProcessExit; + +declare_oxc_lint!( + /// ### What it does + /// Disallow `process.exit()`. + /// + /// ### Why is this bad? + /// Only use `process.exit()` in CLI apps. Throw an error instead. + /// + /// ### Example + /// ```javascript + /// // Bad + /// if (problem) process.exit(1); + /// + /// // Good + /// if (problem) throw new Error("message"); + /// + /// #! /usr/bin/env node + /// if (problem) process.exit(1); + /// ``` + NoProcessExit, + restriction, +); + +impl Rule for NoProcessExit { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::CallExpression(expr) = node.kind() { + if is_method_call(expr, Some(&["process"]), Some(&["exit"]), None, None) { + if has_hashbang(ctx) + || is_inside_process_event_handler(ctx, node) + || is_worker_threads_imported(ctx) + { + return; + } + + ctx.diagnostic(NoProcessExitDiagnostic(expr.span)); + } + } + } +} + +fn has_hashbang(ctx: &LintContext) -> bool { + let Some(root) = ctx.nodes().iter().next() else { return false }; + let AstKind::Program(program) = root.kind() else { return false }; + program.hashbang.is_some() +} + +fn is_inside_process_event_handler(ctx: &LintContext, node: &AstNode) -> bool { + for parent in ctx.nodes().iter_parents(node.id()) { + if let AstKind::CallExpression(expr) = parent.kind() { + if is_method_call(expr, Some(&["process"]), Some(&["on", "once"]), Some(1), None) { + return true; + } + } + } + + false +} + +fn is_worker_threads_imported(ctx: &LintContext) -> bool { + ctx.semantic().module_record().import_entries.iter().any(|entry| { + matches!(entry.module_request.name().as_str(), "worker_threads" | "node:worker_threads") + }) +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("#!/usr/bin/env node\n\nprocess.exit();"), + ("Process.exit()"), + ("const x = process.exit;"), + ("x(process.exit)"), + (r#"process.on("SIGINT", function() { process.exit(1); })"#), + (r#"process.on("SIGKILL", function() { process.exit(1); })"#), + (r#"process.on("SIGINT", () => { process.exit(1); })"#), + (r#"process.on("SIGINT", () => process.exit(1))"#), + (r#"process.on("SIGINT", () => { if (true) { process.exit(1); } })"#), + (r#"process.once("SIGINT", function() { process.exit(1); })"#), + (r#"process.once("SIGKILL", function() { process.exit(1); })"#), + (r#"process.once("SIGINT", () => { process.exit(1); })"#), + (r#"process.once("SIGINT", () => process.exit(1))"#), + (r#"process.once("SIGINT", () => { if (true) { process.exit(1); } })"#), + (r" + const {workerData, parentPort} = require('worker_threads'); + process.exit(1); + "), + (r" + const {workerData, parentPort} = require('node:worker_threads'); + process.exit(1); + "), + (r" + import {workerData, parentPort} from 'worker_threads'; + process.exit(1); + "), + (r" + import foo from 'worker_threads'; + process.exit(1); + "), + (r" + import foo from 'node:worker_threads'; + process.exit(1); + "), + // Not `CallExpression` + ("new process.exit(1);"), + // Not `MemberExpression` + ("exit(1);"), + // `callee.property` is not a `Identifier` + // (r#"process["exit"](1);"#), + // Computed + ("process[exit](1);"), + // Not exit + ("process.foo(1);"), + // Not `process` + ("foo.exit(1);"), + // `callee.object.type` is not a `Identifier` + ("lib.process.exit(1);"), + ]; + + let fail = vec![ + ("process.exit();"), + ("process.exit(1);"), + ("x(process.exit(1));"), + (r#"process.on("SIGINT", function() {});process.exit();"#), + (r#"process.once("SIGINT", function() {}); process.exit(0)"#), + (r" + const mod = require('not_worker_threads'); + process.exit(1); + "), + (r" + import mod from 'not_worker_threads'; + process.exit(1); + "), + // Not `CallExpression` + (r" + const mod = new require('worker_threads'); + process.exit(1); + "), + // Not `Literal` worker_threads + (r" + const mod = require(worker_threads); + process.exit(1); + "), + // Not `CallExpression` + (r#"new process.on("SIGINT", function() { process.exit(1); })"#), + (r#"new process.once("SIGINT", function() { process.exit(1); })"#), + // Not `MemberExpression` + (r#"on("SIGINT", function() { process.exit(1); })"#), + (r#"once("SIGINT", function() { process.exit(1); })"#), + // `callee.property` is not a `Identifier` + // (r#"process["on"]("SIGINT", function() { process.exit(1); })"#), + // (r#"process["once"]("SIGINT", function() { process.exit(1); })"#), + // Computed + (r#"process[on]("SIGINT", function() { process.exit(1); })"#), + (r#"process[once]("SIGINT", function() { process.exit(1); })"#), + // Not `on` / `once` + (r#"process.foo("SIGINT", function() { process.exit(1); })"#), + // Not `process` + (r#"foo.on("SIGINT", function() { process.exit(1); })"#), + (r#"foo.once("SIGINT", function() { process.exit(1); })"#), + // `callee.object.type` is not a `Identifier` + (r#"lib.process.on("SIGINT", function() { process.exit(1); })"#), + (r#"lib.process.once("SIGINT", function() { process.exit(1); })"#), + ]; + + Tester::new(NoProcessExit::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_process_exit.snap b/crates/oxc_linter/src/snapshots/no_process_exit.snap new file mode 100644 index 000000000..0ff600bb7 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_process_exit.snap @@ -0,0 +1,153 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_process_exit +--- + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:1] + 1 │ process.exit(); + · ────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:1] + 1 │ process.exit(1); + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:3] + 1 │ x(process.exit(1)); + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:37] + 1 │ process.on("SIGINT", function() {});process.exit(); + · ────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:40] + 1 │ process.once("SIGINT", function() {}); process.exit(0) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:3:13] + 2 │ const mod = require('not_worker_threads'); + 3 │ process.exit(1); + · ─────────────── + 4 │ + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:3:13] + 2 │ import mod from 'not_worker_threads'; + 3 │ process.exit(1); + · ─────────────── + 4 │ + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:3:13] + 2 │ const mod = new require('worker_threads'); + 3 │ process.exit(1); + · ─────────────── + 4 │ + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:3:13] + 2 │ const mod = require(worker_threads); + 3 │ process.exit(1); + · ─────────────── + 4 │ + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:39] + 1 │ new process.on("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:41] + 1 │ new process.once("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:27] + 1 │ on("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:29] + 1 │ once("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:36] + 1 │ process[on]("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:38] + 1 │ process[once]("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:36] + 1 │ process.foo("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:31] + 1 │ foo.on("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:33] + 1 │ foo.once("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:39] + 1 │ lib.process.on("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. + + ⚠ eslint-plugin-unicorn(no-process-exit): Disallow `process.exit()`. + ╭─[no_process_exit.tsx:1:41] + 1 │ lib.process.once("SIGINT", function() { process.exit(1); }) + · ─────────────── + ╰──── + help: Only use `process.exit()` in CLI apps. Throw an error instead. +