diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 8270658740ba9..6767407aa369c 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1793,6 +1793,11 @@ impl RuleRunner for crate::rules::node::no_new_require::NoNewRequire { const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::node::no_process_env::NoProcessEnv { + const NODE_TYPES: Option<&AstTypesBitset> = None; + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::oxc::approx_constant::ApproxConstant { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::NumericLiteral])); diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 90202c303ffcc..1eaba9acf11be 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -640,6 +640,7 @@ pub(crate) mod vitest { pub(crate) mod node { pub mod no_exports_assign; pub mod no_new_require; + pub mod no_process_env; } pub(crate) mod vue { @@ -961,6 +962,7 @@ oxc_macros::declare_all_lint_rules! { nextjs::no_typos, nextjs::no_unwanted_polyfillio, nextjs::no_html_link_for_pages, + node::no_process_env, node::no_exports_assign, node::no_new_require, oxc::approx_constant, diff --git a/crates/oxc_linter/src/rules/node/no_process_env.rs b/crates/oxc_linter/src/rules/node/no_process_env.rs new file mode 100644 index 0000000000000..7d889fe92f1d4 --- /dev/null +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -0,0 +1,199 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::IsGlobalReference; +use oxc_span::{CompactStr, GetSpan, Span}; +use rustc_hash::FxHashSet; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{AstNode, context::LintContext, rule::Rule}; + +fn no_process_env_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Unexpected use of `process.env`") + .with_help("Remove usage of `process.env`") + .with_label(span) +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[schemars(rename_all = "camelCase")] +struct ConfigElement0 { + allowed_variables: FxHashSet, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] +pub struct NoProcessEnv(Box); + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallows use of `process.env`. + /// + /// ### Why is this bad? + /// + /// Directly reading `process.env` can lead to implicit runtime configuration, + /// make code harder to test, and bypass configuration validation. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// if(process.env.NODE_ENV === "development") { + /// // ... + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// import config from "./config"; + /// + /// if(config.env === "development") { + /// //... + /// } + /// ``` + NoProcessEnv, + node, + restriction, + config = NoProcessEnv, +); + +fn is_process_global_object(object_expr: &oxc_ast::ast::Expression, ctx: &LintContext) -> bool { + let Some(obj_id) = object_expr.get_identifier_reference() else { + return false; + }; + obj_id.is_global_reference_name("process", ctx.scoping()) +} + +impl Rule for NoProcessEnv { + fn from_configuration(value: serde_json::Value) -> Self { + let allowed_variables: FxHashSet = value + .as_array() + .and_then(|arr| arr.first()) + .and_then(|v| v.get("allowedVariables")) + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(CompactStr::from) + .collect::>() + }) + .unwrap_or_default(); + + Self(Box::new(ConfigElement0 { allowed_variables })) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + // Match `process.env` as either static `process.env` or computed `process["env"]` + let mut is_process_env_member = false; + let mut current_span = Span::default(); + + match node.kind() { + AstKind::StaticMemberExpression(mem) => { + if mem.property.name.as_str() == "env" && is_process_global_object(&mem.object, ctx) + { + is_process_env_member = true; + current_span = mem.span; + } + } + AstKind::ComputedMemberExpression(mem) => { + if mem.static_property_name().is_some_and(|name| name.as_str() == "env") + && is_process_global_object(&mem.object, ctx) + { + is_process_env_member = true; + current_span = mem.span; + } + } + _ => {} + } + + if !is_process_env_member { + return; + } + + // Default: report any `process.env` usage + let mut should_report = true; + + // If used as `process.env.ALLOWED` and `ALLOWED` is configured, do not report + match ctx.nodes().parent_kind(node.id()) { + AstKind::StaticMemberExpression(parent_mem) => { + if let Some(obj_mem) = parent_mem.object.as_member_expression() + && obj_mem.span() == current_span + { + let (.., prop_name) = parent_mem.static_property_info(); + if self.0.allowed_variables.contains(prop_name) { + should_report = false; + } + } + } + AstKind::ComputedMemberExpression(parent_mem) => { + if let Some(obj_mem) = parent_mem.object.as_member_expression() + && obj_mem.span() == current_span + && let Some((_, name)) = parent_mem.static_property_info() + && self.0.allowed_variables.contains(name) + { + should_report = false; + } + } + _ => {} + } + + if should_report { + ctx.diagnostic(no_process_env_diagnostic(current_span)); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("Process.env", None), + ("process[env]", None), + ("process.nextTick", None), + ("process.execArgv", None), + ("process.env.NODE_ENV", Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }]))), + ( + "process.env['NODE_ENV']", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ( + "process['env'].NODE_ENV", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ( + "process['env']['NODE_ENV']", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ]; + + let fail = vec![ + ("process.env", None), + ("process['env']", None), + ("process.env.ENV", None), + ("f(process.env)", None), + ( + "process.env['OTHER_VARIABLE']", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ( + "process.env.OTHER_VARIABLE", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ( + "process['env']['OTHER_VARIABLE']", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ( + "process['env'].OTHER_VARIABLE", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ("process.env[NODE_ENV]", Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }]))), + ( + "process['env'][NODE_ENV]", + Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])), + ), + ]; + + Tester::new(NoProcessEnv::NAME, NoProcessEnv::PLUGIN, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/node_no_process_env.snap b/crates/oxc_linter/src/snapshots/node_no_process_env.snap new file mode 100644 index 0000000000000..2dda0057fddbd --- /dev/null +++ b/crates/oxc_linter/src/snapshots/node_no_process_env.snap @@ -0,0 +1,72 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process.env + · ─────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process['env'] + · ────────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process.env.ENV + · ─────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:3] + 1 │ f(process.env) + · ─────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process.env['OTHER_VARIABLE'] + · ─────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process.env.OTHER_VARIABLE + · ─────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process['env']['OTHER_VARIABLE'] + · ────────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process['env'].OTHER_VARIABLE + · ────────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process.env[NODE_ENV] + · ─────────── + ╰──── + help: Remove usage of `process.env` + + ⚠ eslint-plugin-node(no-process-env): Unexpected use of `process.env` + ╭─[no_process_env.tsx:1:1] + 1 │ process['env'][NODE_ENV] + · ────────────── + ╰──── + help: Remove usage of `process.env`