oxc/crates/oxc_linter/src/rules/eslint/no_obj_calls.rs
2024-07-21 23:35:47 +00:00

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();
}