Skip to content

Commit 66e0b1f

Browse files
committed
feat(linter): implement unicorn/prefer-global-this (#11197)
1 parent b26554b commit 66e0b1f

3 files changed

Lines changed: 845 additions & 0 deletions

File tree

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ mod unicorn {
400400
pub mod prefer_dom_node_remove;
401401
pub mod prefer_dom_node_text_content;
402402
pub mod prefer_event_target;
403+
pub mod prefer_global_this;
403404
pub mod prefer_includes;
404405
pub mod prefer_logical_operator_over_ternary;
405406
pub mod prefer_math_min_max;
@@ -1037,6 +1038,7 @@ oxc_macros::declare_all_lint_rules! {
10371038
unicorn::no_zero_fractions,
10381039
unicorn::number_literal_case,
10391040
unicorn::numeric_separators_style,
1041+
unicorn::prefer_global_this,
10401042
unicorn::prefer_object_from_entries,
10411043
unicorn::prefer_array_find,
10421044
unicorn::prefer_array_index_of,
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{Expression, MemberExpression},
4+
};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_macros::declare_oxc_lint;
7+
use oxc_span::{GetSpan, Span};
8+
9+
use crate::{AstNode, context::LintContext, rule::Rule};
10+
11+
fn prefer_global_this_diagnostic(span: Span) -> OxcDiagnostic {
12+
OxcDiagnostic::warn("Prefer `globalThis` over environment-specific global aliases like `window`, `self`, and `global`.")
13+
.with_help("Replace the alias with `globalThis`.")
14+
.with_label(span)
15+
}
16+
17+
#[derive(Debug, Default, Clone)]
18+
pub struct PreferGlobalThis;
19+
20+
declare_oxc_lint!(
21+
/// ### What it does
22+
///
23+
/// Enforces the use of [`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) instead of
24+
/// environment‑specific global object aliases (`window`, `self`, or `global`).
25+
/// Using the standard `globalThis` makes your code portable across browsers, Web Workers, Node.js,
26+
/// and future JavaScript runtimes.
27+
///
28+
/// ### Why is this bad?
29+
///
30+
/// • **Portability** – `window` is only defined in browser main threads, `self` is used in Web Workers,
31+
/// and `global` is Node‑specific. Choosing the wrong alias causes runtime crashes when the code is
32+
/// executed outside of its original environment.
33+
/// • **Clarity** – `globalThis` clearly communicates that you are referring to the global object itself
34+
/// rather than a particular platform.
35+
///
36+
/// ### Examples
37+
///
38+
/// Examples of **incorrect** code for this rule:
39+
/// ```js
40+
/// // Browser‑only
41+
/// window.alert("Hi");
42+
///
43+
/// // Node‑only
44+
/// if (typeof global.Buffer !== "undefined") {}
45+
///
46+
/// // Web Worker‑only
47+
/// self.postMessage("done");
48+
/// ```
49+
///
50+
/// Examples of **correct** code for this rule:
51+
/// ```js
52+
/// globalThis.alert("Hi");
53+
///
54+
/// if (typeof globalThis.Buffer !== "undefined") {}
55+
///
56+
/// globalThis.postMessage("done");
57+
/// ```
58+
PreferGlobalThis,
59+
unicorn,
60+
style,
61+
pending
62+
);
63+
64+
impl Rule for PreferGlobalThis {
65+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
66+
let AstKind::IdentifierReference(ident) = node.kind() else { return };
67+
68+
if !matches!(ident.name.as_str(), "window" | "self" | "global")
69+
|| is_computed_member_expression_object(node, ctx)
70+
|| !ctx.scoping().root_unresolved_references().contains_key(&ident.name.as_str())
71+
{
72+
return;
73+
}
74+
75+
if let Some(AstKind::MemberExpression(MemberExpression::StaticMemberExpression(e))) =
76+
ctx.nodes().parent_kind(node.id())
77+
{
78+
if let Expression::Identifier(ident) = &e.object {
79+
if ident.name == "self"
80+
&& WEB_WORKER_SPECIFIC_APIS.contains(&e.property.name.as_str())
81+
{
82+
return;
83+
}
84+
85+
if ident.name == "window"
86+
&& WINDOW_SPECIFIC_APIS.contains(&e.property.name.as_str())
87+
{
88+
if matches!(
89+
e.property.name.as_str(),
90+
"addEventListener" | "removeEventListener" | "dispatchEvent"
91+
) {
92+
if let Some(AstKind::CallExpression(call_expr)) =
93+
ctx.nodes().ancestor_kinds(node.id()).nth(2)
94+
{
95+
if let Some(Expression::StringLiteral(lit)) =
96+
call_expr.arguments.first().and_then(|arg| arg.as_expression())
97+
{
98+
if WINDOW_SPECIFIC_EVENTS.contains(&lit.value.as_str()) {
99+
return;
100+
}
101+
}
102+
} else {
103+
return;
104+
}
105+
} else {
106+
return;
107+
}
108+
}
109+
}
110+
}
111+
112+
ctx.diagnostic(prefer_global_this_diagnostic(ident.span));
113+
}
114+
}
115+
116+
/// `window[foo]`, `self[bar]`, etc. are allowed.
117+
fn is_computed_member_expression_object(node: &AstNode<'_>, ctx: &LintContext<'_>) -> bool {
118+
if let Some(AstKind::MemberExpression(member_expr)) = ctx.nodes().parent_kind(node.id()) {
119+
if !member_expr.is_computed() {
120+
return false;
121+
}
122+
if let Expression::Identifier(obj_ident) = &member_expr.object().get_inner_expression() {
123+
return obj_ident.span == node.kind().span();
124+
}
125+
}
126+
false
127+
}
128+
129+
const WEB_WORKER_SPECIFIC_APIS: &[&str] = &[
130+
// https://html.spec.whatwg.org/multipage/workers.html#the-workerglobalscope-common-interface
131+
"addEventListener",
132+
"removeEventListener",
133+
"dispatchEvent",
134+
"self",
135+
"location",
136+
"navigator",
137+
"onerror",
138+
"onlanguagechange",
139+
"onoffline",
140+
"ononline",
141+
"onrejectionhandled",
142+
"onunhandledrejection",
143+
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-dedicatedworkerglobalscope-interface
144+
"name",
145+
"postMessage",
146+
"onconnect",
147+
];
148+
149+
const WINDOW_SPECIFIC_APIS: &[&str] = &[
150+
// Properties and methods
151+
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-window-object
152+
"name",
153+
"locationbar",
154+
"menubar",
155+
"personalbar",
156+
"scrollbars",
157+
"statusbar",
158+
"toolbar",
159+
"status",
160+
"close",
161+
"closed",
162+
"stop",
163+
"focus",
164+
"blur",
165+
"frames",
166+
"length",
167+
"top",
168+
"opener",
169+
"parent",
170+
"frameElement",
171+
"open",
172+
"originAgentCluster",
173+
"postMessage",
174+
// Events commonly associated with "window"
175+
"onresize",
176+
"onblur",
177+
"onfocus",
178+
"onload",
179+
"onscroll",
180+
"onscrollend",
181+
"onwheel",
182+
"onbeforeunload",
183+
"onmessage",
184+
"onmessageerror",
185+
"onpagehide",
186+
"onpagereveal",
187+
"onpageshow",
188+
"onpageswap",
189+
"onunload",
190+
// To add/remove/dispatch events that are commonly associated with "window"
191+
// https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow
192+
"addEventListener",
193+
"removeEventListener",
194+
"dispatchEvent",
195+
// https://dom.spec.whatwg.org/#idl-index
196+
"event", // Deprecated and quirky, best left untouched
197+
// https://drafts.csswg.org/cssom-view/#idl-index
198+
"screen",
199+
"visualViewport",
200+
"moveTo",
201+
"moveBy",
202+
"resizeTo",
203+
"resizeBy",
204+
"innerWidth",
205+
"innerHeight",
206+
"outerWidth",
207+
"outerHeight",
208+
"scrollX",
209+
"pageXOffset",
210+
"scrollY",
211+
"pageYOffset",
212+
"scroll",
213+
"scrollTo",
214+
"scrollBy",
215+
"screenX",
216+
"screenLeft",
217+
"screenY",
218+
"screenTop",
219+
"screenWidth",
220+
"screenHeight",
221+
"devicePixelRatio",
222+
];
223+
224+
// Allow `on<event>` where <event> is in the windowSpecificEvents set from reference implementation.
225+
const WINDOW_SPECIFIC_EVENTS: &[&str] = &[
226+
"resize",
227+
"blur",
228+
"focus",
229+
"load",
230+
"scroll",
231+
"scrollend",
232+
"wheel",
233+
"beforeunload",
234+
"message",
235+
"messageerror",
236+
"pagehide",
237+
"pagereveal",
238+
"pageshow",
239+
"pageswap",
240+
"unload",
241+
];
242+
243+
#[test]
244+
fn test() {
245+
use crate::tester::Tester;
246+
247+
let pass = vec![
248+
"globalThis",
249+
"globalThis.foo",
250+
"globalThis[foo]",
251+
"globalThis.foo()",
252+
"const { foo } = globalThis",
253+
"function foo (window) {}",
254+
"function foo (global) {}",
255+
"var foo = function foo (window) {}",
256+
"var foo = function foo (global) {}",
257+
"var window = {}",
258+
"let global = {}",
259+
"const global = {}",
260+
"function foo (window) {
261+
window.foo();
262+
}",
263+
"var window = {};
264+
function foo () {
265+
window.foo();
266+
}",
267+
"foo.window",
268+
"foo.global",
269+
r#"import window from "xxx""#,
270+
r#"import * as window from "xxx""#,
271+
r#"import window, {foo} from "xxx""#,
272+
r#"export { window } from "xxx""#,
273+
r#"export * as window from "xxx";"#,
274+
"try {
275+
} catch (window) {}",
276+
r#"window.name = "foo""#,
277+
"window.addEventListener",
278+
"window.innerWidth",
279+
"window.innerHeight",
280+
"self.location",
281+
"self.navigator",
282+
r#"window.addEventListener("resize", () => {})"#,
283+
"window.onresize = function () {}",
284+
"const {window} = jsdom()
285+
window.jQuery = jQuery;",
286+
"({ foo: window.name } = {})",
287+
"[window.name] = []",
288+
"window[foo]",
289+
"window[title]",
290+
r#"window["foo"]"#,
291+
];
292+
293+
let fail = vec![
294+
"global",
295+
"self",
296+
"window",
297+
"window.foo",
298+
"window.foo()",
299+
"window > 10",
300+
"10 > window",
301+
"window ?? 10",
302+
"10 ?? window",
303+
"window.foo = 123",
304+
"window = 123",
305+
"obj.a = window",
306+
"function* gen() {
307+
yield window
308+
}",
309+
"async function gen() {
310+
await window
311+
}",
312+
"window ? foo : bar",
313+
"foo ? window : bar",
314+
"foo ? bar : window",
315+
"function foo() {
316+
return window
317+
}",
318+
"new window()",
319+
"const obj = {
320+
foo: window.foo,
321+
bar: window.bar,
322+
window: window
323+
}",
324+
"function sequenceTest() {
325+
let x, y;
326+
x = (y = 10, y + 5, window);
327+
console.log(x, y);
328+
}",
329+
"window`Hello ${42} World`",
330+
"tag`Hello ${window.foo} World`",
331+
"var str = `hello ${window.foo} world!`",
332+
"delete window.foo",
333+
"++window",
334+
"++window.foo",
335+
"for (var attr in window) {
336+
}",
337+
"for (window.foo = 0; i < 10; window.foo++) {
338+
}",
339+
"for (const item of window.foo) {
340+
}",
341+
"for (const item of window) {
342+
}",
343+
"switch (window) {}",
344+
"switch (true) {
345+
case window:
346+
break;
347+
}",
348+
"switch (true) {
349+
case window.foo:
350+
break;
351+
}",
352+
"while (window) {
353+
}",
354+
"do {} while (window) {}",
355+
"if (window) {}",
356+
"throw window",
357+
"var foo = window",
358+
"function foo (name = window) {
359+
}",
360+
"self.innerWidth",
361+
"self.innerHeight",
362+
"window.crypto",
363+
r#"window.addEventListener("play", () => {})"#,
364+
"window.onplay = function () {}",
365+
"function greet({ name = window.foo }) {}",
366+
"({ foo: window.foo } = {})",
367+
"[window.foo] = []",
368+
"foo[window]",
369+
"foo[window.foo]",
370+
r#"typeof window !== "undefined""#,
371+
r#"typeof self !== "undefined""#,
372+
r#"typeof global !== "undefined""#,
373+
r#"typeof window.something === "function""#,
374+
r#"typeof self.something === "function""#,
375+
r#"typeof global.something === "function""#,
376+
"global.global_did_not_declare_in_language_options",
377+
"window.window_did_not_declare_in_language_options",
378+
"self.self_did_not_declare_in_language_options",
379+
];
380+
381+
Tester::new(PreferGlobalThis::NAME, PreferGlobalThis::PLUGIN, pass, fail).test_and_snapshot();
382+
}

0 commit comments

Comments
 (0)