From 8923ba4bd2a56839874559cd8d9325c8e530e170 Mon Sep 17 00:00:00 2001 From: skovhus Date: Sat, 11 Oct 2025 23:06:49 +0200 Subject: [PATCH 1/8] feat(linter): scaffold node/no-process-env --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/node/no_process_env.rs | 122 ++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 crates/oxc_linter/src/rules/node/no_process_env.rs diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 90202c303ffcc..0272927c7db53 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -638,6 +638,7 @@ pub(crate) mod vitest { } pub(crate) mod node { + pub mod no_process_env; pub mod no_exports_assign; pub mod no_new_require; } @@ -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..1cc1c040d5c04 --- /dev/null +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -0,0 +1,122 @@ +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + AstNode, + context::LintContext, + fixer::{RuleFix, RuleFixer}, + rule::Rule, +}; + +fn no_process_env_diagnostic(span: Span) -> OxcDiagnostic { + // See for details + OxcDiagnostic::warn("Should be an imperative statement about what is wrong") + .with_help("Should be a command-like statement that tells the user how to fix the issue") + .with_label(span) +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[schemars(rename_all = "camelCase")] +struct ConfigElement0 { + allowed_variables: Vec, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] +pub struct NoProcessEnv(ConfigElement0); + +// See for documentation details. +declare_oxc_lint!( + /// ### What it does + /// + /// Briefly describe the rule's purpose. + /// + /// ### Why is this bad? + /// + /// Explain why violating this rule is problematic. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ``` + NoProcessEnv, + node, + nursery, // TODO: change category to `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, or `style` + // See for details + pending // TODO: describe fix capabilities. Remove if no fix can be done, + // keep at 'pending' if you think one could be added but don't know how. + // Options are 'fix', 'fix_dangerous', 'suggestion', and 'conditional_fix_suggestion' + config = NoProcessEnv, +); + +impl Rule for NoProcessEnv { + fn from_configuration(value: serde_json::Value) -> Self { + serde_json::from_value(value).unwrap() + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {} +} + +#[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(); +} From 7e0d70785cfdb4ce06e783071c18be14cce6c501 Mon Sep 17 00:00:00 2001 From: skovhus Date: Sat, 11 Oct 2025 23:23:12 +0200 Subject: [PATCH 2/8] feat(linter): implement node/no_process_env --- crates/oxc_linter/src/rules.rs | 2 +- .../src/rules/node/no_process_env.rs | 134 ++++++++++++++---- .../src/snapshots/node_no_process_env.snap | 72 ++++++++++ 3 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 crates/oxc_linter/src/snapshots/node_no_process_env.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0272927c7db53..1eaba9acf11be 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -638,9 +638,9 @@ pub(crate) mod vitest { } pub(crate) mod node { - pub mod no_process_env; pub mod no_exports_assign; pub mod no_new_require; + pub mod no_process_env; } pub(crate) mod vue { diff --git a/crates/oxc_linter/src/rules/node/no_process_env.rs b/crates/oxc_linter/src/rules/node/no_process_env.rs index 1cc1c040d5c04..df2cb0efb6fba 100644 --- a/crates/oxc_linter/src/rules/node/no_process_env.rs +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -1,69 +1,155 @@ +use oxc_ast::{AstKind, AstType}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +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, - fixer::{RuleFix, RuleFixer}, - rule::Rule, -}; +use crate::{AstNode, context::LintContext, rule::Rule}; fn no_process_env_diagnostic(span: Span) -> OxcDiagnostic { - // See for details - OxcDiagnostic::warn("Should be an imperative statement about what is wrong") - .with_help("Should be a command-like statement that tells the user how to fix the issue") + 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: Vec, + allowed_variables: FxHashSet, } #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] -pub struct NoProcessEnv(ConfigElement0); +pub struct NoProcessEnv(Box); -// See for documentation details. declare_oxc_lint!( /// ### What it does /// - /// Briefly describe the rule's purpose. + /// Disallows use of `process.env`. /// /// ### Why is this bad? /// - /// Explain why violating this rule is problematic. + /// 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 - /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// if(process.env.NODE_ENV === "development") { + /// // ... + /// } /// ``` /// /// Examples of **correct** code for this rule: /// ```js - /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// import config from "./config"; + /// + /// if(config.env === "development") { + /// //... + /// } /// ``` NoProcessEnv, node, - nursery, // TODO: change category to `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, or `style` - // See for details - pending // TODO: describe fix capabilities. Remove if no fix can be done, - // keep at 'pending' if you think one could be added but don't know how. - // Options are 'fix', 'fix_dangerous', 'suggestion', and 'conditional_fix_suggestion' + 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 { - serde_json::from_value(value).unwrap() + 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(&CompactStr::new(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 + { + if let Some((_, name)) = parent_mem.static_property_info() { + if self.0.allowed_variables.contains(&CompactStr::new(name)) { + should_report = false; + } + } + } + } + _ => {} + } + + if should_report { + ctx.diagnostic(no_process_env_diagnostic(current_span)); + } } +} - fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {} +impl crate::rule::RuleRunner for NoProcessEnv { + const NODE_TYPES: Option<&'static AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::StaticMemberExpression, + AstType::ComputedMemberExpression, + ])); } #[test] 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` From a9cb415bbb6108869e4f3150a8d933b317fb2791 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:14:24 +0000 Subject: [PATCH 3/8] [autofix.ci] apply automated fixes --- crates/oxc_linter/src/rules/node/no_process_env.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/rules/node/no_process_env.rs b/crates/oxc_linter/src/rules/node/no_process_env.rs index df2cb0efb6fba..eb71f72d2ea33 100644 --- a/crates/oxc_linter/src/rules/node/no_process_env.rs +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -120,7 +120,7 @@ impl Rule for NoProcessEnv { && obj_mem.span() == current_span { let (.., prop_name) = parent_mem.static_property_info(); - if self.0.allowed_variables.contains(&CompactStr::new(prop_name)) { + if self.0.allowed_variables.contains(&CompactStr::new(prop_name)) { should_report = false; } } From 4614c88007b3aab33ac1402d6d8cab9b524e36c4 Mon Sep 17 00:00:00 2001 From: skovhus Date: Mon, 13 Oct 2025 14:30:11 +0200 Subject: [PATCH 4/8] chore: avoid creating new string --- crates/oxc_linter/src/rules/node/no_process_env.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/oxc_linter/src/rules/node/no_process_env.rs b/crates/oxc_linter/src/rules/node/no_process_env.rs index eb71f72d2ea33..d469927972783 100644 --- a/crates/oxc_linter/src/rules/node/no_process_env.rs +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -120,7 +120,7 @@ impl Rule for NoProcessEnv { && obj_mem.span() == current_span { let (.., prop_name) = parent_mem.static_property_info(); - if self.0.allowed_variables.contains(&CompactStr::new(prop_name)) { + if self.0.allowed_variables.contains(prop_name) { should_report = false; } } @@ -130,7 +130,7 @@ impl Rule for NoProcessEnv { && obj_mem.span() == current_span { if let Some((_, name)) = parent_mem.static_property_info() { - if self.0.allowed_variables.contains(&CompactStr::new(name)) { + if self.0.allowed_variables.contains(name) { should_report = false; } } From 690a502df599161dbabccdc05993002822907f38 Mon Sep 17 00:00:00 2001 From: skovhus Date: Mon, 13 Oct 2025 14:33:43 +0200 Subject: [PATCH 5/8] chore: run cargo lintgen --- crates/oxc_linter/src/generated/rule_runner_impls.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 8270658740ba9..77cbe817ac9bc 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1793,6 +1793,10 @@ 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; +} + impl RuleRunner for crate::rules::oxc::approx_constant::ApproxConstant { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::NumericLiteral])); From ea1231993bd4ccd0c0de55d8437fbcb15aa7902e Mon Sep 17 00:00:00 2001 From: skovhus Date: Mon, 13 Oct 2025 14:50:01 +0200 Subject: [PATCH 6/8] chore: remove leftover --- crates/oxc_linter/src/rules/node/no_process_env.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/oxc_linter/src/rules/node/no_process_env.rs b/crates/oxc_linter/src/rules/node/no_process_env.rs index d469927972783..215cc2e5d777d 100644 --- a/crates/oxc_linter/src/rules/node/no_process_env.rs +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -1,4 +1,4 @@ -use oxc_ast::{AstKind, AstType}; +use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_semantic::IsGlobalReference; @@ -145,13 +145,6 @@ impl Rule for NoProcessEnv { } } -impl crate::rule::RuleRunner for NoProcessEnv { - const NODE_TYPES: Option<&'static AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ - AstType::StaticMemberExpression, - AstType::ComputedMemberExpression, - ])); -} - #[test] fn test() { use crate::tester::Tester; From b1383611798274b7a2cbfc82e41951ae4082d6f2 Mon Sep 17 00:00:00 2001 From: skovhus Date: Mon, 13 Oct 2025 14:57:42 +0200 Subject: [PATCH 7/8] chore: lint --- crates/oxc_linter/src/rules/node/no_process_env.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/rules/node/no_process_env.rs b/crates/oxc_linter/src/rules/node/no_process_env.rs index 215cc2e5d777d..7d889fe92f1d4 100644 --- a/crates/oxc_linter/src/rules/node/no_process_env.rs +++ b/crates/oxc_linter/src/rules/node/no_process_env.rs @@ -128,12 +128,10 @@ impl Rule for NoProcessEnv { 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) { - if let Some((_, name)) = parent_mem.static_property_info() { - if self.0.allowed_variables.contains(name) { - should_report = false; - } - } + should_report = false; } } _ => {} From 880abb525fcb405974d807c944eac5d5800bbf81 Mon Sep 17 00:00:00 2001 From: skovhus Date: Mon, 13 Oct 2025 15:10:26 +0200 Subject: [PATCH 8/8] chore: re-run lintgen --- crates/oxc_linter/src/generated/rule_runner_impls.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 77cbe817ac9bc..6767407aa369c 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1795,6 +1795,7 @@ impl RuleRunner for crate::rules::node::no_new_require::NoNewRequire { 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 {