diff --git a/.changeset/bracket-bun-env-support.md b/.changeset/bracket-bun-env-support.md new file mode 100644 index 000000000000..aed43863493d --- /dev/null +++ b/.changeset/bracket-bun-env-support.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#8494](https://github.com/biomejs/biome/issues/8494). Extended [`noUndeclaredEnvVars`](https://biomejs.dev/linter/rules/no-undeclared-env-vars/) to support bracket notation (`process.env["VAR"]`, `import.meta.env["VAR"]`), Bun runtime (`Bun.env.VAR`, `Bun.env["VAR"]`), and Deno runtime (`Deno.env.get("VAR")`). diff --git a/crates/biome_js_analyze/src/lint/nursery/no_undeclared_env_vars.rs b/crates/biome_js_analyze/src/lint/nursery/no_undeclared_env_vars.rs index dcdea309f73c..bf895871867e 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_undeclared_env_vars.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_undeclared_env_vars.rs @@ -3,8 +3,11 @@ use biome_analyze::{ }; use biome_console::markup; use biome_diagnostics::Severity; -use biome_js_syntax::{AnyJsExpression, JsStaticMemberExpression, global_identifier}; -use biome_rowan::{AstNode, TokenText}; +use biome_js_syntax::{ + AnyJsExpression, AnyJsMemberExpression, JsCallExpression, JsComputedMemberExpression, + JsStaticMemberExpression, global_identifier, inner_string_text, +}; +use biome_rowan::{AstNode, AstSeparatedList, TokenText}; use biome_rule_options::no_undeclared_env_vars::NoUndeclaredEnvVarsOptions; use crate::services::turborepo::Turborepo; @@ -17,8 +20,13 @@ declare_lint_rule! { /// Using undeclared environment variables can lead to incorrect cache hits /// and unpredictable build behavior. /// - /// This rule checks for `process.env.VAR_NAME` and `import.meta.env.VAR_NAME` - /// accesses and validates them against: + /// This rule checks for environment variable accesses in the following patterns: + /// - `process.env.VAR_NAME` and `process.env["VAR_NAME"]` + /// - `import.meta.env.VAR_NAME` and `import.meta.env["VAR_NAME"]` + /// - `Bun.env.VAR_NAME` and `Bun.env["VAR_NAME"]` + /// - `Deno.env.get("VAR_NAME")` + /// + /// It validates them against: /// 1. Environment variables declared in `turbo.json(c)` (`globalEnv`, `globalPassThroughEnv`, task-level `env`, and task-level `passThroughEnv`) /// 2. Environment variables specified in the rule's `allowedEnvVars` option /// 3. Default allowed variables (common system vars and framework-specific patterns) @@ -92,7 +100,7 @@ pub struct EnvVarAccess { } impl Rule for NoUndeclaredEnvVars { - type Query = Turborepo; + type Query = Turborepo; type State = EnvVarAccess; type Signals = Option; type Options = NoUndeclaredEnvVarsOptions; @@ -103,50 +111,36 @@ impl Rule for NoUndeclaredEnvVars { return None; } - let static_member_expr = ctx.query(); + let member_expr = ctx.query(); let options = ctx.options(); let model = ctx.model(); - // Check if this is either process.env.SOMETHING or import.meta.env.SOMETHING - let object = static_member_expr.object().ok()?; - let parent_member = object.as_js_static_member_expression()?; - let env_member = parent_member.member().ok()?; - - // Must be accessing ".env" - if env_member.as_js_name()?.to_trimmed_text().text() != "env" { - return None; - } - - let parent_object = parent_member.object().ok()?; - - let is_process_env = is_global_process_object(&parent_object, model); - let is_import_meta_env = is_import_meta_object(&parent_object); - - if !is_process_env && !is_import_meta_env { - return None; + // Check if this is a Deno.env.get() call + if let Some(deno_result) = match_deno_env_get(member_expr, model) { + return match deno_result { + Some(env_var_name) => check_env_var(ctx, env_var_name, options), + // Matched Deno.env.get(...) but couldn't statically resolve the key (e.g. Deno.env.get(key)) + // so skip it. + None => None, + }; } - // Get the env var name being accessed (e.g., NODE_ENV from process.env.NODE_ENV) - let member = static_member_expr.member().ok()?; - let env_var_name = member.as_js_name()?.value_token().ok()?; - let env_var_text = env_var_name.token_text_trimmed(); - let env_var = env_var_text.text(); - - // Check if this env var is allowed by default patterns or user options - if is_env_var_allowed_by_defaults(env_var) - || is_env_var_allowed_by_options(env_var, options) - { - return None; - } + // Get the env var name and the parent object based on the expression type + let (env_var_name, parent_object) = match member_expr { + AnyJsMemberExpression::JsStaticMemberExpression(static_expr) => { + extract_from_static_member(static_expr)? + } + AnyJsMemberExpression::JsComputedMemberExpression(computed_expr) => { + extract_from_computed_member(computed_expr)? + } + }; - // Check if declared in turbo.json - if ctx.is_env_var_declared(env_var) { + // Check if the parent object is a valid env access (process.env, import.meta.env, or Bun.env) + if !is_global_env_object(&parent_object, model) && !is_import_meta_object(&parent_object) { return None; } - Some(EnvVarAccess { - env_var_name: env_var_text, - }) + check_env_var(ctx, env_var_name, options) } fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { @@ -163,17 +157,142 @@ impl Rule for NoUndeclaredEnvVars { } } -/// Checks if the object is the global `process` identifier (not shadowed by a local binding) -fn is_global_process_object( - expr: &AnyJsExpression, - model: &biome_js_semantic::SemanticModel, -) -> bool { +/// Extracts the env var name and parent object from a static member expression (e.g., `process.env.MY_VAR`) +fn extract_from_static_member( + static_expr: &JsStaticMemberExpression, +) -> Option<(TokenText, AnyJsExpression)> { + // Check if this is process.env.SOMETHING, import.meta.env.SOMETHING, or Bun.env.SOMETHING + let object = static_expr.object().ok()?; + let parent_member = object.as_js_static_member_expression()?; + let env_member = parent_member.member().ok()?; + + // Must be accessing ".env" + if env_member.as_js_name()?.to_trimmed_text().text() != "env" { + return None; + } + + let parent_object = parent_member.object().ok()?; + + // Get the env var name being accessed (e.g., NODE_ENV from process.env.NODE_ENV) + let member = static_expr.member().ok()?; + let env_var_name = member.as_js_name()?.value_token().ok()?; + let env_var_text = env_var_name.token_text_trimmed(); + + Some((env_var_text, parent_object)) +} + +/// Extracts the env var name and parent object from a computed member expression (e.g., `process.env["MY_VAR"]`) +/// Only handles string literal keys; dynamic keys like `process.env[key]` are skipped +fn extract_from_computed_member( + computed_expr: &JsComputedMemberExpression, +) -> Option<(TokenText, AnyJsExpression)> { + // Check if this is process.env["SOMETHING"], import.meta.env["SOMETHING"], or Bun.env["SOMETHING"] + let object = computed_expr.object().ok()?; + let parent_member = object.as_js_static_member_expression()?; + let env_member = parent_member.member().ok()?; + + // Must be accessing ".env" + if env_member.as_js_name()?.to_trimmed_text().text() != "env" { + return None; + } + + let parent_object = parent_member.object().ok()?; + + // Get the env var name from the computed member + // Only process string literals, skip dynamic accesses like process.env[key] + let member = computed_expr.member().ok()?; + let member = member.omit_parentheses(); + + // Check if it's a string literal + let string_literal = member + .as_any_js_literal_expression()? + .as_js_string_literal_expression()?; + let string_token = string_literal.value_token().ok()?; + let env_var_name = inner_string_text(&string_token); + + Some((env_var_name, parent_object)) +} + +/// Checks if the object is a global env object (`process` or `Bun`, not shadowed by a local binding) +fn is_global_env_object(expr: &AnyJsExpression, model: &biome_js_semantic::SemanticModel) -> bool { let Some((reference, name)) = global_identifier(expr) else { return false; }; - // Check that it's named "process" and not bound to a local variable - name.text() == "process" && model.binding(&reference).is_none() + // Check that it's named "process" or "Bun" and not bound to a local variable + let name_text = name.text(); + (name_text == "process" || name_text == "Bun") && model.binding(&reference).is_none() +} + +fn match_deno_env_get( + member_expr: &AnyJsMemberExpression, + model: &biome_js_semantic::SemanticModel, +) -> Option> { + let AnyJsMemberExpression::JsStaticMemberExpression(static_expr) = member_expr else { + return None; + }; + + let get_member = static_expr.member().ok()?; + if get_member.as_js_name()?.to_trimmed_text().text() != "get" { + return None; + } + + let object = static_expr.object().ok()?.omit_parentheses(); + let env_member = object.as_js_static_member_expression()?; + if env_member + .member() + .ok()? + .as_js_name()? + .to_trimmed_text() + .text() + != "env" + { + return None; + } + + let deno_object = env_member.object().ok()?.omit_parentheses(); + let Some((reference, name)) = global_identifier(&deno_object) else { + return Some(None); + }; + if name.text() != "Deno" || model.binding(&reference).is_some() { + return Some(None); + } + + let Some(parent) = member_expr.syntax().parent() else { + return Some(None); + }; + let Some(call) = JsCallExpression::cast(parent) else { + return Some(None); + }; + if call.is_optional() { + return Some(None); + } + + let first_arg = call.arguments().ok()?.args().first()?.ok()?; + let first_arg = first_arg.as_any_js_expression()?.clone().omit_parentheses(); + let string_literal = first_arg + .as_any_js_literal_expression()? + .as_js_string_literal_expression()?; + let string_token = string_literal.value_token().ok()?; + + Some(Some(inner_string_text(&string_token))) +} + +fn check_env_var( + ctx: &RuleContext, + env_var_name: TokenText, + options: &NoUndeclaredEnvVarsOptions, +) -> Option { + let env_var = env_var_name.text(); + if is_env_var_allowed_by_defaults(env_var) || is_env_var_allowed_by_options(env_var, options) { + return None; + } + + if ctx.is_env_var_declared(env_var) { + return None; + } + + Some(EnvVarAccess { env_var_name }) } /// Checks if the object is `import.meta` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js index 96c9c4592eea..882acd01a63d 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js @@ -7,3 +7,15 @@ const acmeSecret = process.env.ACME_SECRET; // Also invalid with import.meta.env const importMetaVar = import.meta.env.CUSTOM_META_VAR; + +// Bracket notation with string literals should also be checked +const bracketVar = process.env["BRACKET_VAR"]; +const bracketMeta = import.meta.env["BRACKET_META_VAR"]; + +// Bun.env should also be checked +const bunVar = Bun.env.BUN_CUSTOM_VAR; +const bunBracketVar = Bun.env["BUN_BRACKET_VAR"]; + +// Deno.env.get should also be checked +const denoVar = Deno.env.get("DENO_CUSTOM_VAR"); +const denoVar2 = Deno.env.get("ANOTHER_DENO_VAR"); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js.snap index 91dc2130b956..d3a2c2c45270 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/invalid.js.snap @@ -14,6 +14,18 @@ const acmeSecret = process.env.ACME_SECRET; // Also invalid with import.meta.env const importMetaVar = import.meta.env.CUSTOM_META_VAR; +// Bracket notation with string literals should also be checked +const bracketVar = process.env["BRACKET_VAR"]; +const bracketMeta = import.meta.env["BRACKET_META_VAR"]; + +// Bun.env should also be checked +const bunVar = Bun.env.BUN_CUSTOM_VAR; +const bunBracketVar = Bun.env["BUN_BRACKET_VAR"]; + +// Deno.env.get should also be checked +const denoVar = Deno.env.get("DENO_CUSTOM_VAR"); +const denoVar2 = Deno.env.get("ANOTHER_DENO_VAR"); + ``` # Diagnostics @@ -70,6 +82,93 @@ invalid.js:9:23 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━ > 9 │ const importMetaVar = import.meta.env.CUSTOM_META_VAR; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10 │ + 11 │ // Bracket notation with string literals should also be checked + + +``` + +``` +invalid.js:12:20 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The environment variable BRACKET_VAR is not listed as a dependency in turbo.json. Add this environment variable a task's 'env'/'passThroughEnv', or to 'globalEnv', 'globalPassThroughEnv', in your turbo.json(c) configuration to ensure correct caching behavior in Turborepo. + + 11 │ // Bracket notation with string literals should also be checked + > 12 │ const bracketVar = process.env["BRACKET_VAR"]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 13 │ const bracketMeta = import.meta.env["BRACKET_META_VAR"]; + 14 │ + + +``` + +``` +invalid.js:13:21 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The environment variable BRACKET_META_VAR is not listed as a dependency in turbo.json. Add this environment variable a task's 'env'/'passThroughEnv', or to 'globalEnv', 'globalPassThroughEnv', in your turbo.json(c) configuration to ensure correct caching behavior in Turborepo. + + 11 │ // Bracket notation with string literals should also be checked + 12 │ const bracketVar = process.env["BRACKET_VAR"]; + > 13 │ const bracketMeta = import.meta.env["BRACKET_META_VAR"]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ + 15 │ // Bun.env should also be checked + + +``` + +``` +invalid.js:16:16 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The environment variable BUN_CUSTOM_VAR is not listed as a dependency in turbo.json. Add this environment variable a task's 'env'/'passThroughEnv', or to 'globalEnv', 'globalPassThroughEnv', in your turbo.json(c) configuration to ensure correct caching behavior in Turborepo. + + 15 │ // Bun.env should also be checked + > 16 │ const bunVar = Bun.env.BUN_CUSTOM_VAR; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 17 │ const bunBracketVar = Bun.env["BUN_BRACKET_VAR"]; + 18 │ + + +``` + +``` +invalid.js:17:23 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The environment variable BUN_BRACKET_VAR is not listed as a dependency in turbo.json. Add this environment variable a task's 'env'/'passThroughEnv', or to 'globalEnv', 'globalPassThroughEnv', in your turbo.json(c) configuration to ensure correct caching behavior in Turborepo. + + 15 │ // Bun.env should also be checked + 16 │ const bunVar = Bun.env.BUN_CUSTOM_VAR; + > 17 │ const bunBracketVar = Bun.env["BUN_BRACKET_VAR"]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 18 │ + 19 │ // Deno.env.get should also be checked + + +``` + +``` +invalid.js:20:17 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The environment variable DENO_CUSTOM_VAR is not listed as a dependency in turbo.json. Add this environment variable a task's 'env'/'passThroughEnv', or to 'globalEnv', 'globalPassThroughEnv', in your turbo.json(c) configuration to ensure correct caching behavior in Turborepo. + + 19 │ // Deno.env.get should also be checked + > 20 │ const denoVar = Deno.env.get("DENO_CUSTOM_VAR"); + │ ^^^^^^^^^^^^ + 21 │ const denoVar2 = Deno.env.get("ANOTHER_DENO_VAR"); + 22 │ + + +``` + +``` +invalid.js:21:18 lint/nursery/noUndeclaredEnvVars ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The environment variable ANOTHER_DENO_VAR is not listed as a dependency in turbo.json. Add this environment variable a task's 'env'/'passThroughEnv', or to 'globalEnv', 'globalPassThroughEnv', in your turbo.json(c) configuration to ensure correct caching behavior in Turborepo. + + 19 │ // Deno.env.get should also be checked + 20 │ const denoVar = Deno.env.get("DENO_CUSTOM_VAR"); + > 21 │ const denoVar2 = Deno.env.get("ANOTHER_DENO_VAR"); + │ ^^^^^^^^^^^^ + 22 │ ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js index ff4e01f29a46..0f37f34e8a27 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js @@ -22,8 +22,36 @@ const expo = process.env.EXPO_PUBLIC_KEY; const viteEnv = import.meta.env.VITE_BASE_URL; const nodeEnvMeta = import.meta.env.NODE_ENV; +// Bracket notation with allowed variables +const nodeEnvBracket = process.env["NODE_ENV"]; +const ciBracket = process.env["CI"]; +const viteBracket = import.meta.env["VITE_API_KEY"]; + +// Bun.env with allowed patterns +const bunNodeEnv = Bun.env.NODE_ENV; +const bunCi = Bun.env.CI; +const bunVite = Bun.env.VITE_APP_KEY; +const bunNodeEnvBracket = Bun.env["NODE_ENV"]; + // Local process variable (not global) - should not be flagged function test() { const process = { env: { LOCAL_VAR: 'test' } }; return process.env.LOCAL_VAR; } + +// Local Bun variable (not global) - should not be flagged +function testBun() { + const Bun = { env: { LOCAL_VAR: 'test' } }; + return Bun.env.LOCAL_VAR; +} + +// Deno.env.get with allowed patterns +const denoNodeEnv = Deno.env.get("NODE_ENV"); +const denoCi = Deno.env.get("CI"); +const denoVite = Deno.env.get("VITE_APP_KEY"); + +// Local Deno variable (not global) - should not be flagged +function testDeno() { + const Deno = { env: { get: () => 'test' } }; + return Deno.env.get("LOCAL_VAR"); +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js.snap index 213833c958ad..5d0124e34918 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/valid.js.snap @@ -28,10 +28,38 @@ const expo = process.env.EXPO_PUBLIC_KEY; const viteEnv = import.meta.env.VITE_BASE_URL; const nodeEnvMeta = import.meta.env.NODE_ENV; +// Bracket notation with allowed variables +const nodeEnvBracket = process.env["NODE_ENV"]; +const ciBracket = process.env["CI"]; +const viteBracket = import.meta.env["VITE_API_KEY"]; + +// Bun.env with allowed patterns +const bunNodeEnv = Bun.env.NODE_ENV; +const bunCi = Bun.env.CI; +const bunVite = Bun.env.VITE_APP_KEY; +const bunNodeEnvBracket = Bun.env["NODE_ENV"]; + // Local process variable (not global) - should not be flagged function test() { const process = { env: { LOCAL_VAR: 'test' } }; return process.env.LOCAL_VAR; } +// Local Bun variable (not global) - should not be flagged +function testBun() { + const Bun = { env: { LOCAL_VAR: 'test' } }; + return Bun.env.LOCAL_VAR; +} + +// Deno.env.get with allowed patterns +const denoNodeEnv = Deno.env.get("NODE_ENV"); +const denoCi = Deno.env.get("CI"); +const denoVite = Deno.env.get("VITE_APP_KEY"); + +// Local Deno variable (not global) - should not be flagged +function testDeno() { + const Deno = { env: { get: () => 'test' } }; + return Deno.env.get("LOCAL_VAR"); +} + ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js index b8dab516e39f..9d18b9c34f52 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js @@ -1,13 +1,10 @@ -/* should not generate diagnostics - dynamic property access is not checked */ +/* should not generate diagnostics - truly dynamic property access is not checked */ -// Dynamic property access cannot be statically analyzed, so these are skipped +// Dynamic property access with variables cannot be statically analyzed, so these are skipped const key = "MY_VAR"; const dynamicVar = process.env[key]; -// Computed property with string literal -const computedVar = process.env["ANOTHER_VAR"]; - -// Dynamic with template literal +// Dynamic with template literal containing interpolation const prefix = "ACME"; const templateVar = process.env[`${prefix}_TOKEN`]; @@ -19,9 +16,15 @@ function getEnvVar(name) { return process.env[name]; } -// Also with import.meta.env +// Also with import.meta.env - dynamic access const dynamicMeta = import.meta.env[key]; -const computedMeta = import.meta.env["CUSTOM_VAR"]; -// These are valid because the rule only checks static member expressions -// (process.env.VAR_NAME), not computed/dynamic access (process.env[key]) +// Also with Bun.env - dynamic access +const dynamicBun = Bun.env[key]; + +// Also with Deno.env.get - dynamic access +const dynamicDeno = Deno.env.get(key); + +// These are valid because the rule cannot statically determine the key +// Note: String literal bracket access like process.env["VAR"] IS now checked +// Note: String literal in Deno.env.get("VAR") IS now checked diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js.snap index 8793e42af937..8d8eea170517 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js.snap @@ -4,16 +4,13 @@ expression: validDynamicAccess.js --- # Input ```js -/* should not generate diagnostics - dynamic property access is not checked */ +/* should not generate diagnostics - truly dynamic property access is not checked */ -// Dynamic property access cannot be statically analyzed, so these are skipped +// Dynamic property access with variables cannot be statically analyzed, so these are skipped const key = "MY_VAR"; const dynamicVar = process.env[key]; -// Computed property with string literal -const computedVar = process.env["ANOTHER_VAR"]; - -// Dynamic with template literal +// Dynamic with template literal containing interpolation const prefix = "ACME"; const templateVar = process.env[`${prefix}_TOKEN`]; @@ -25,11 +22,17 @@ function getEnvVar(name) { return process.env[name]; } -// Also with import.meta.env +// Also with import.meta.env - dynamic access const dynamicMeta = import.meta.env[key]; -const computedMeta = import.meta.env["CUSTOM_VAR"]; -// These are valid because the rule only checks static member expressions -// (process.env.VAR_NAME), not computed/dynamic access (process.env[key]) +// Also with Bun.env - dynamic access +const dynamicBun = Bun.env[key]; + +// Also with Deno.env.get - dynamic access +const dynamicDeno = Deno.env.get(key); + +// These are valid because the rule cannot statically determine the key +// Note: String literal bracket access like process.env["VAR"] IS now checked +// Note: String literal in Deno.env.get("VAR") IS now checked ```