mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
221 lines
7.3 KiB
Rust
221 lines
7.3 KiB
Rust
use oxc_ast::{
|
|
ast::{match_member_expression, Expression, IdentifierReference, MemberExpression},
|
|
AstKind,
|
|
};
|
|
use oxc_diagnostics::OxcDiagnostic;
|
|
use oxc_macros::declare_oxc_lint;
|
|
use oxc_semantic::{AstNode, ScopeId};
|
|
use oxc_span::Span;
|
|
|
|
use crate::{context::LintContext, rule::Rule};
|
|
|
|
const GLOBAL_THIS: &str = "globalThis";
|
|
const NON_CALLABLE_GLOBALS: [&str; 5] = ["Atomics", "Intl", "JSON", "Math", "Reflect"];
|
|
|
|
fn no_obj_calls_diagnostic(x0: &str, span1: Span) -> OxcDiagnostic {
|
|
OxcDiagnostic::warn("Disallow calling some global objects as functions")
|
|
.with_help(format!("{x0} is not a function."))
|
|
.with_label(span1)
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct NoObjCalls;
|
|
|
|
impl Default for NoObjCalls {
|
|
fn default() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
declare_oxc_lint! {
|
|
/// ### What it does
|
|
/// Disallow calling some global objects as functions
|
|
///
|
|
/// ### Why is this bad?
|
|
/// Some global objects are not intended to be called as functions.
|
|
/// Calling them as functions will usually result in a TypeError being thrown.
|
|
///
|
|
/// ### Example
|
|
/// ```javascript
|
|
/// // Bad
|
|
/// let math = Math();
|
|
/// let newMath = new Math();
|
|
///
|
|
/// let json = JSON();
|
|
/// let newJson = new JSON();
|
|
///
|
|
/// let atomics = Atomics();
|
|
/// let newAtomics = new Atomics();
|
|
///
|
|
/// let intl = Intl();
|
|
/// let newIntl = new Intl();
|
|
///
|
|
/// let reflect = Reflect();
|
|
/// let newReflect = new Reflect();
|
|
///
|
|
/// // Good
|
|
/// let area = r => 2 * Math.PI * r * r;
|
|
/// let object = JSON.parse("{}");
|
|
/// let first = Atomics.load(sharedArray, 0);
|
|
/// let segmenterFrom = Intl.Segmenter("fr", { granularity: "word" });
|
|
/// ```
|
|
NoObjCalls,
|
|
correctness,
|
|
}
|
|
|
|
fn is_global_obj(s: &str) -> bool {
|
|
NON_CALLABLE_GLOBALS.contains(&s)
|
|
}
|
|
|
|
fn global_this_member<'a>(expr: &'a MemberExpression<'_>) -> Option<&'a str> {
|
|
if expr.object().is_specific_id(GLOBAL_THIS) {
|
|
expr.static_property_name()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn resolve_global_binding<'a, 'b: 'a>(
|
|
ident: &'a oxc_allocator::Box<'a, IdentifierReference<'a>>,
|
|
scope_id: ScopeId,
|
|
ctx: &LintContext<'a>,
|
|
) -> Option<&'a str> {
|
|
if ctx.semantic().is_reference_to_global_variable(ident) {
|
|
Some(ident.name.as_str())
|
|
} else {
|
|
let scope = ctx.scopes();
|
|
let nodes = ctx.nodes();
|
|
let symbols = ctx.symbols();
|
|
scope.ancestors(scope_id).find_map(|id| scope.get_binding(id, &ident.name)).map_or_else(
|
|
// panic in debug builds, but fail gracefully in release builds
|
|
|| {
|
|
debug_assert!(
|
|
false,
|
|
"No binding id found for {}, but this IdentifierReference
|
|
is not a global",
|
|
&ident.name
|
|
);
|
|
None
|
|
},
|
|
|binding_id| {
|
|
let decl = nodes.get_node(symbols.get_declaration(binding_id));
|
|
let decl_scope = decl.scope_id();
|
|
match decl.kind() {
|
|
AstKind::VariableDeclarator(parent_decl) => {
|
|
if !parent_decl.id.kind.is_binding_identifier() {
|
|
return Some(ident.name.as_str());
|
|
}
|
|
match &parent_decl.init {
|
|
// handles "let a = JSON; let b = a; a();"
|
|
Some(Expression::Identifier(parent_ident))
|
|
if parent_ident.name != ident.name =>
|
|
{
|
|
resolve_global_binding(parent_ident, decl_scope, ctx)
|
|
}
|
|
// handles "let a = globalThis.JSON; let b = a; a();"
|
|
Some(parent_expr) if parent_expr.is_member_expression() => {
|
|
global_this_member(parent_expr.to_member_expression())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Rule for NoObjCalls {
|
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
|
#[allow(clippy::needless_return)]
|
|
let (callee, span) = match node.kind() {
|
|
AstKind::NewExpression(expr) => (&expr.callee, expr.span),
|
|
AstKind::CallExpression(expr) => (&expr.callee, expr.span),
|
|
_ => return,
|
|
};
|
|
|
|
match callee {
|
|
Expression::Identifier(ident) => {
|
|
// handle new Math(), Math(), etc
|
|
if let Some(top_level_reference) =
|
|
resolve_global_binding(ident, node.scope_id(), ctx)
|
|
{
|
|
if is_global_obj(top_level_reference) {
|
|
ctx.diagnostic(no_obj_calls_diagnostic(ident.name.as_str(), span));
|
|
}
|
|
}
|
|
}
|
|
|
|
match_member_expression!(Expression) => {
|
|
// handle new globalThis.Math(), globalThis.Math(), etc
|
|
if let Some(global_member) = global_this_member(callee.to_member_expression()) {
|
|
if is_global_obj(global_member) {
|
|
ctx.diagnostic(no_obj_calls_diagnostic(global_member, span));
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
// noop
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test() {
|
|
use crate::tester::Tester;
|
|
// see: https://github.com/eslint/eslint/blob/main/tests/lib/rules/no-obj-calls.js
|
|
|
|
let pass = vec![
|
|
("const m = Math;", None),
|
|
("let m = foo.Math();", None),
|
|
("JSON.parse(\"{}\")", None),
|
|
("Math.PI * 2 * (r * r)", None),
|
|
("bar.Atomics(foo)", None),
|
|
// reference test cases
|
|
(
|
|
"let j = JSON;
|
|
function foo() {
|
|
let j = x => x;
|
|
return x();
|
|
}",
|
|
None,
|
|
),
|
|
// https://github.com/oxc-project/oxc/pull/508#issuecomment-1618850742
|
|
("{const Math = () => {}; {let obj = new Math();}}", None),
|
|
("{const {parse} = JSON;parse('{}')}", None),
|
|
// https://github.com/oxc-project/oxc/issues/4389
|
|
(
|
|
r"
|
|
export const getConfig = getConfig;
|
|
getConfig();",
|
|
None,
|
|
),
|
|
];
|
|
|
|
let fail = vec![
|
|
("let newObj = new JSON();", None),
|
|
("let obj = JSON();", None),
|
|
("let obj = globalThis.JSON()", None),
|
|
("new JSON", None),
|
|
("const foo = x => new JSON()", None),
|
|
("let newObj = new Math();", None),
|
|
("let obj = Math();", None),
|
|
("let obj = new Math().foo;", None),
|
|
("let obj = new globalThis.Math()", None),
|
|
("let newObj = new Atomics();", None),
|
|
("let obj = Atomics();", None),
|
|
("let newObj = new Intl();", None),
|
|
("let obj = Intl();", None),
|
|
("let newObj = new Reflect();", None),
|
|
("let obj = Reflect();", None),
|
|
("function d() { JSON.parse(Atomics()) }", None),
|
|
// reference test cases
|
|
("let j = JSON; j();", None),
|
|
("let a = JSON; let b = a; let c = b; b();", None),
|
|
("let m = globalThis.Math; new m();", None),
|
|
];
|
|
|
|
Tester::new(NoObjCalls::NAME, pass, fail).test_and_snapshot();
|
|
}
|