diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index 048772747c7e93..955bb193a40bdc 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -157,6 +157,19 @@ impl<'a> LintContext<'a> { && !self.globals().get(name).is_some_and(|value| *value == GlobalValue::Off) } + /// Checks if the provided identifier is a reference to a global variable. + pub fn get_global_variable_value(&self, name: &str) -> Option { + if !self.scopes().root_unresolved_references().contains_key(name) { + return None; + } + + if let Some(value) = self.globals().get(name) { + return Some(*value); + } + + self.get_env_global_entry(name) + } + /// Runtime environments turned on/off by the user. /// /// Examples of environments are `builtin`, `browser`, `node`, etc. @@ -165,6 +178,23 @@ impl<'a> LintContext<'a> { &self.parent.config.env } + fn get_env_global_entry(&self, var: &str) -> Option { + // builtin is always readonly + if GLOBALS["builtin"].contains_key(var) { + return Some(GlobalValue::Readonly); + } + + for env in self.env().iter() { + if let Some(env) = GLOBALS.get(env) { + if let Some(value) = env.get(var) { + return Some(GlobalValue::from(*value)); + }; + } + } + + None + } + /// Checks if a given variable named is defined as a global variable in the current environment. /// /// Example: diff --git a/crates/oxc_linter/src/javascript_globals.rs b/crates/oxc_linter/src/javascript_globals.rs index da2002aef7aa6c..00ab732979a340 100644 --- a/crates/oxc_linter/src/javascript_globals.rs +++ b/crates/oxc_linter/src/javascript_globals.rs @@ -26,6 +26,7 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "Int32Array" => false, "Int8Array" => false, "Intl" => false, + "Iterator" => false, "JSON" => false, "Map" => false, "Math" => false, @@ -432,6 +433,7 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "CSSKeywordValue" => false, "CSSLayerBlockRule" => false, "CSSLayerStatementRule" => false, + "CSSMarginRule" => false, "CSSMathClamp" => false, "CSSMathInvert" => false, "CSSMathMax" => false, @@ -443,6 +445,7 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "CSSMatrixComponent" => false, "CSSMediaRule" => false, "CSSNamespaceRule" => false, + "CSSNestedDeclarations" => false, "CSSNumericArray" => false, "CSSNumericValue" => false, "CSSPageDescriptors" => false, @@ -754,7 +757,6 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "InputEvent" => false, "IntersectionObserver" => false, "IntersectionObserverEntry" => false, - "Iterator" => false, "Keyboard" => false, "KeyboardEvent" => false, "KeyboardLayoutMap" => false, @@ -1216,12 +1218,15 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "XRDOMOverlayState" => false, "XRDepthInformation" => false, "XRFrame" => false, + "XRHand" => false, "XRHitTestResult" => false, "XRHitTestSource" => false, "XRInputSource" => false, "XRInputSourceArray" => false, "XRInputSourceEvent" => false, "XRInputSourcesChangeEvent" => false, + "XRJointPose" => false, + "XRJointSpace" => false, "XRLayer" => false, "XRLightEstimate" => false, "XRLightProbe" => false, @@ -1489,6 +1494,7 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "BroadcastChannel" => false, "Buffer" => false, "ByteLengthQueuingStrategy" => false, + "CloseEvent" => false, "CompressionStream" => false, "CountQueuingStrategy" => false, "Crypto" => false, @@ -1501,7 +1507,6 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "File" => false, "FormData" => false, "Headers" => false, - "Iterator" => false, "MessageChannel" => false, "MessageEvent" => false, "MessagePort" => false, @@ -1564,6 +1569,7 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "Blob" => false, "BroadcastChannel" => false, "ByteLengthQueuingStrategy" => false, + "CloseEvent" => false, "CompressionStream" => false, "CountQueuingStrategy" => false, "Crypto" => false, @@ -1576,7 +1582,6 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "File" => false, "FormData" => false, "Headers" => false, - "Iterator" => false, "MessageChannel" => false, "MessageEvent" => false, "MessagePort" => false, @@ -1718,6 +1723,10 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "GPUTextureView" => false, "GPUUncapturedErrorEvent" => false, "GPUValidationError" => false, + "HID" => false, + "HIDConnectionEvent" => false, + "HIDDevice" => false, + "HIDInputReportEvent" => false, "Headers" => false, "IDBCursor" => false, "IDBCursorWithValue" => false, @@ -1737,7 +1746,6 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "ImageDecoder" => false, "ImageTrack" => false, "ImageTrackList" => false, - "Iterator" => false, "Lock" => false, "LockManager" => false, "MediaCapabilities" => false, @@ -1772,6 +1780,7 @@ pub static GLOBALS: Map<&'static str, Map<&'static str, bool>> = phf_map! { "PushManager" => false, "PushSubscription" => false, "PushSubscriptionOptions" => false, + "RTCDataChannel" => false, "RTCEncodedAudioFrame" => false, "RTCEncodedVideoFrame" => false, "ReadableByteStreamController" => false, diff --git a/crates/oxc_linter/src/rules/eslint/no_global_assign.rs b/crates/oxc_linter/src/rules/eslint/no_global_assign.rs index e67a997a5c39e3..2b680c6a3b7031 100644 --- a/crates/oxc_linter/src/rules/eslint/no_global_assign.rs +++ b/crates/oxc_linter/src/rules/eslint/no_global_assign.rs @@ -2,7 +2,7 @@ use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::{CompactStr, Span}; -use crate::{context::LintContext, rule::Rule}; +use crate::{config::GlobalValue, context::LintContext, rule::Rule}; fn no_global_assign_diagnostic(global_name: &str, span: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!("Read-only global '{global_name}' should not be modified.")) @@ -65,7 +65,9 @@ impl Rule for NoGlobalAssign { let reference = symbol_table.get_reference(reference_id); if reference.is_write() && !self.excludes.iter().any(|n| n == name) - && ctx.env_contains_var(name) + && ctx + .get_global_variable_value(name) + .is_some_and(|global| global == GlobalValue::Readonly) { ctx.diagnostic(no_global_assign_diagnostic( name, @@ -82,28 +84,69 @@ fn test() { use crate::tester::Tester; let pass = vec![ - ("string='1';", None), - ("var string;", None), - ("Object = 0;", Some(serde_json::json!([{ "exceptions": ["Object"] }]))), - ("top = 0;", None), - // ("onload = 0;", None), // env: { browser: true } - ("require = 0;", None), - ("window[parseInt('42', 10)] = 99;", None), - // ("a = 1", None), // globals: { a: true } }, + ("string='1';", None, None), + ("var string;", None, None), + ("Object = 0;", Some(serde_json::json!([{ "exceptions": ["Object"] }])), None), + ("top = 0;", None, None), + ( + "onload = 0;", + None, + Some(serde_json::json!({ + "env": { + "browser": true + } + })), + ), + ("require = 0;", None, None), + ("window[parseInt('42', 10)] = 99;", None, None), + ( + "a = 1", + None, + Some(serde_json::json!({ + "globals": { + "a": true + } + })), + ), // ("/*global a:true*/ a = 1", None), ]; let fail = vec![ - ("String = 'hello world';", None), - ("String++;", None), - ("({Object = 0, String = 0} = {});", None), - // ("top = 0;", None), // env: { browser: true }, - // ("require = 0;", None), // env: { node: true }, - ("function f() { Object = 1; }", None), + ("String = 'hello world';", None, None), + ("String++;", None, None), + ("({Object = 0, String = 0} = {});", None, None), + ( + "top = 0;", + None, + Some(serde_json::json!({ + "env": { + "browser": true + } + })), + ), + ( + "require = 0;", + None, + Some(serde_json::json!({ + "env": { + "node": true + } + })), + ), + ("function f() { Object = 1; }", None, None), + ( + "a = 1", + None, + Some(serde_json::json!({ + "globals": { + "a": false + } + })), + ), // ("/*global b:false*/ function f() { b = 1; }", None), // ("/*global b:false*/ function f() { b++; }", None), // ("/*global b*/ b = 1;", None), - ("Array = 1;", None), + ("Array = 1;", None, None), ]; Tester::new(NoGlobalAssign::NAME, NoGlobalAssign::PLUGIN, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/eslint_no_global_assign.snap b/crates/oxc_linter/src/snapshots/eslint_no_global_assign.snap index c3a581fe31271f..c17200843bf654 100644 --- a/crates/oxc_linter/src/snapshots/eslint_no_global_assign.snap +++ b/crates/oxc_linter/src/snapshots/eslint_no_global_assign.snap @@ -29,6 +29,20 @@ source: crates/oxc_linter/src/tester.rs · ╰── Read-only global 'Object' should not be modified. ╰──── + ⚠ eslint(no-global-assign): Read-only global 'top' should not be modified. + ╭─[no_global_assign.tsx:1:1] + 1 │ top = 0; + · ─┬─ + · ╰── Read-only global 'top' should not be modified. + ╰──── + + ⚠ eslint(no-global-assign): Read-only global 'require' should not be modified. + ╭─[no_global_assign.tsx:1:1] + 1 │ require = 0; + · ───┬─── + · ╰── Read-only global 'require' should not be modified. + ╰──── + ⚠ eslint(no-global-assign): Read-only global 'Object' should not be modified. ╭─[no_global_assign.tsx:1:16] 1 │ function f() { Object = 1; } @@ -36,6 +50,13 @@ source: crates/oxc_linter/src/tester.rs · ╰── Read-only global 'Object' should not be modified. ╰──── + ⚠ eslint(no-global-assign): Read-only global 'a' should not be modified. + ╭─[no_global_assign.tsx:1:1] + 1 │ a = 1 + · ┬ + · ╰── Read-only global 'a' should not be modified. + ╰──── + ⚠ eslint(no-global-assign): Read-only global 'Array' should not be modified. ╭─[no_global_assign.tsx:1:1] 1 │ Array = 1;