Skip to content

Commit 452d128

Browse files
feat(linter): node/no_process_env (#14536)
Followed the guide in https://oxc.rs/docs/contribute/linter/adding-rules.html, and vibe-coded this. It's my first lines of Rust, so please review carefully. Based on https://github.com/eslint-community/eslint-plugin-n/blob/master/lib/rules/no-process-env.js --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4f301de commit 452d128

File tree

4 files changed

+278
-0
lines changed

4 files changed

+278
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,11 @@ impl RuleRunner for crate::rules::node::no_new_require::NoNewRequire {
17931793
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
17941794
}
17951795

1796+
impl RuleRunner for crate::rules::node::no_process_env::NoProcessEnv {
1797+
const NODE_TYPES: Option<&AstTypesBitset> = None;
1798+
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
1799+
}
1800+
17961801
impl RuleRunner for crate::rules::oxc::approx_constant::ApproxConstant {
17971802
const NODE_TYPES: Option<&AstTypesBitset> =
17981803
Some(&AstTypesBitset::from_types(&[AstType::NumericLiteral]));

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ pub(crate) mod vitest {
640640
pub(crate) mod node {
641641
pub mod no_exports_assign;
642642
pub mod no_new_require;
643+
pub mod no_process_env;
643644
}
644645

645646
pub(crate) mod vue {
@@ -961,6 +962,7 @@ oxc_macros::declare_all_lint_rules! {
961962
nextjs::no_typos,
962963
nextjs::no_unwanted_polyfillio,
963964
nextjs::no_html_link_for_pages,
965+
node::no_process_env,
964966
node::no_exports_assign,
965967
node::no_new_require,
966968
oxc::approx_constant,
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
use oxc_ast::AstKind;
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_semantic::IsGlobalReference;
5+
use oxc_span::{CompactStr, GetSpan, Span};
6+
use rustc_hash::FxHashSet;
7+
use schemars::JsonSchema;
8+
use serde::{Deserialize, Serialize};
9+
10+
use crate::{AstNode, context::LintContext, rule::Rule};
11+
12+
fn no_process_env_diagnostic(span: Span) -> OxcDiagnostic {
13+
OxcDiagnostic::warn("Unexpected use of `process.env`")
14+
.with_help("Remove usage of `process.env`")
15+
.with_label(span)
16+
}
17+
18+
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
19+
#[schemars(rename_all = "camelCase")]
20+
struct ConfigElement0 {
21+
allowed_variables: FxHashSet<CompactStr>,
22+
}
23+
24+
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
25+
pub struct NoProcessEnv(Box<ConfigElement0>);
26+
27+
declare_oxc_lint!(
28+
/// ### What it does
29+
///
30+
/// Disallows use of `process.env`.
31+
///
32+
/// ### Why is this bad?
33+
///
34+
/// Directly reading `process.env` can lead to implicit runtime configuration,
35+
/// make code harder to test, and bypass configuration validation.
36+
///
37+
/// ### Examples
38+
///
39+
/// Examples of **incorrect** code for this rule:
40+
/// ```js
41+
/// if(process.env.NODE_ENV === "development") {
42+
/// // ...
43+
/// }
44+
/// ```
45+
///
46+
/// Examples of **correct** code for this rule:
47+
/// ```js
48+
/// import config from "./config";
49+
///
50+
/// if(config.env === "development") {
51+
/// //...
52+
/// }
53+
/// ```
54+
NoProcessEnv,
55+
node,
56+
restriction,
57+
config = NoProcessEnv,
58+
);
59+
60+
fn is_process_global_object(object_expr: &oxc_ast::ast::Expression, ctx: &LintContext) -> bool {
61+
let Some(obj_id) = object_expr.get_identifier_reference() else {
62+
return false;
63+
};
64+
obj_id.is_global_reference_name("process", ctx.scoping())
65+
}
66+
67+
impl Rule for NoProcessEnv {
68+
fn from_configuration(value: serde_json::Value) -> Self {
69+
let allowed_variables: FxHashSet<CompactStr> = value
70+
.as_array()
71+
.and_then(|arr| arr.first())
72+
.and_then(|v| v.get("allowedVariables"))
73+
.and_then(|v| v.as_array())
74+
.map(|arr| {
75+
arr.iter()
76+
.filter_map(|v| v.as_str())
77+
.map(CompactStr::from)
78+
.collect::<FxHashSet<CompactStr>>()
79+
})
80+
.unwrap_or_default();
81+
82+
Self(Box::new(ConfigElement0 { allowed_variables }))
83+
}
84+
85+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
86+
// Match `process.env` as either static `process.env` or computed `process["env"]`
87+
let mut is_process_env_member = false;
88+
let mut current_span = Span::default();
89+
90+
match node.kind() {
91+
AstKind::StaticMemberExpression(mem) => {
92+
if mem.property.name.as_str() == "env" && is_process_global_object(&mem.object, ctx)
93+
{
94+
is_process_env_member = true;
95+
current_span = mem.span;
96+
}
97+
}
98+
AstKind::ComputedMemberExpression(mem) => {
99+
if mem.static_property_name().is_some_and(|name| name.as_str() == "env")
100+
&& is_process_global_object(&mem.object, ctx)
101+
{
102+
is_process_env_member = true;
103+
current_span = mem.span;
104+
}
105+
}
106+
_ => {}
107+
}
108+
109+
if !is_process_env_member {
110+
return;
111+
}
112+
113+
// Default: report any `process.env` usage
114+
let mut should_report = true;
115+
116+
// If used as `process.env.ALLOWED` and `ALLOWED` is configured, do not report
117+
match ctx.nodes().parent_kind(node.id()) {
118+
AstKind::StaticMemberExpression(parent_mem) => {
119+
if let Some(obj_mem) = parent_mem.object.as_member_expression()
120+
&& obj_mem.span() == current_span
121+
{
122+
let (.., prop_name) = parent_mem.static_property_info();
123+
if self.0.allowed_variables.contains(prop_name) {
124+
should_report = false;
125+
}
126+
}
127+
}
128+
AstKind::ComputedMemberExpression(parent_mem) => {
129+
if let Some(obj_mem) = parent_mem.object.as_member_expression()
130+
&& obj_mem.span() == current_span
131+
&& let Some((_, name)) = parent_mem.static_property_info()
132+
&& self.0.allowed_variables.contains(name)
133+
{
134+
should_report = false;
135+
}
136+
}
137+
_ => {}
138+
}
139+
140+
if should_report {
141+
ctx.diagnostic(no_process_env_diagnostic(current_span));
142+
}
143+
}
144+
}
145+
146+
#[test]
147+
fn test() {
148+
use crate::tester::Tester;
149+
150+
let pass = vec![
151+
("Process.env", None),
152+
("process[env]", None),
153+
("process.nextTick", None),
154+
("process.execArgv", None),
155+
("process.env.NODE_ENV", Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }]))),
156+
(
157+
"process.env['NODE_ENV']",
158+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
159+
),
160+
(
161+
"process['env'].NODE_ENV",
162+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
163+
),
164+
(
165+
"process['env']['NODE_ENV']",
166+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
167+
),
168+
];
169+
170+
let fail = vec![
171+
("process.env", None),
172+
("process['env']", None),
173+
("process.env.ENV", None),
174+
("f(process.env)", None),
175+
(
176+
"process.env['OTHER_VARIABLE']",
177+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
178+
),
179+
(
180+
"process.env.OTHER_VARIABLE",
181+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
182+
),
183+
(
184+
"process['env']['OTHER_VARIABLE']",
185+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
186+
),
187+
(
188+
"process['env'].OTHER_VARIABLE",
189+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
190+
),
191+
("process.env[NODE_ENV]", Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }]))),
192+
(
193+
"process['env'][NODE_ENV]",
194+
Some(serde_json::json!([{ "allowedVariables": ["NODE_ENV"] }])),
195+
),
196+
];
197+
198+
Tester::new(NoProcessEnv::NAME, NoProcessEnv::PLUGIN, pass, fail).test_and_snapshot();
199+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
5+
╭─[no_process_env.tsx:1:1]
6+
1process.env
7+
· ───────────
8+
╰────
9+
help: Remove usage of `process.env`
10+
11+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
12+
╭─[no_process_env.tsx:1:1]
13+
1process['env']
14+
· ──────────────
15+
╰────
16+
help: Remove usage of `process.env`
17+
18+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
19+
╭─[no_process_env.tsx:1:1]
20+
1process.env.ENV
21+
· ───────────
22+
╰────
23+
help: Remove usage of `process.env`
24+
25+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
26+
╭─[no_process_env.tsx:1:3]
27+
1f(process.env)
28+
· ───────────
29+
╰────
30+
help: Remove usage of `process.env`
31+
32+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
33+
╭─[no_process_env.tsx:1:1]
34+
1process.env['OTHER_VARIABLE']
35+
· ───────────
36+
╰────
37+
help: Remove usage of `process.env`
38+
39+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
40+
╭─[no_process_env.tsx:1:1]
41+
1process.env.OTHER_VARIABLE
42+
· ───────────
43+
╰────
44+
help: Remove usage of `process.env`
45+
46+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
47+
╭─[no_process_env.tsx:1:1]
48+
1process['env']['OTHER_VARIABLE']
49+
· ──────────────
50+
╰────
51+
help: Remove usage of `process.env`
52+
53+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
54+
╭─[no_process_env.tsx:1:1]
55+
1process['env'].OTHER_VARIABLE
56+
· ──────────────
57+
╰────
58+
help: Remove usage of `process.env`
59+
60+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
61+
╭─[no_process_env.tsx:1:1]
62+
1process.env[NODE_ENV]
63+
· ───────────
64+
╰────
65+
help: Remove usage of `process.env`
66+
67+
eslint-plugin-node(no-process-env): Unexpected use of `process.env`
68+
╭─[no_process_env.tsx:1:1]
69+
1process['env'][NODE_ENV]
70+
· ──────────────
71+
╰────
72+
help: Remove usage of `process.env`

0 commit comments

Comments
 (0)