diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index c7ff4df6e..2bf0efd91 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -446,6 +446,26 @@ impl<'a> MemberExpression<'a> { } } + pub fn static_property_info(&'a self) -> Option<(Span, &'a str)> { + match self { + MemberExpression::ComputedMemberExpression(expr) => match &expr.expression { + Expression::StringLiteral(lit) => Some((lit.span, &lit.value)), + Expression::TemplateLiteral(lit) => { + if lit.expressions.is_empty() && lit.quasis.len() == 1 { + Some((lit.span, &lit.quasis[0].value.raw)) + } else { + None + } + } + _ => None, + }, + MemberExpression::StaticMemberExpression(expr) => { + Some((expr.property.span, &expr.property.name)) + } + MemberExpression::PrivateFieldExpression(_) => None, + } + } + /// Whether it is a static member access `object.property` pub fn is_specific_member_access(&'a self, object: &str, property: &str) -> bool { self.object().is_specific_id(object) diff --git a/crates/oxc_linter/src/rules/no_eval.rs b/crates/oxc_linter/src/rules/no_eval.rs index 12df12442..721271e1b 100644 --- a/crates/oxc_linter/src/rules/no_eval.rs +++ b/crates/oxc_linter/src/rules/no_eval.rs @@ -1,4 +1,6 @@ -use oxc_ast::AstKind; +// Ported from https://github.com/eslint/eslint/tree/main/lib/rules/no-eval.js + +use oxc_ast::{ast::Expression, AstKind}; use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, @@ -12,10 +14,26 @@ use crate::{context::LintContext, rule::Rule}; #[derive(Debug, Error, Diagnostic)] #[error("eslint(no-eval): eval can be harmful.")] #[diagnostic(severity(warning))] -struct NoEvalDiagnostic(#[label("eval can be harmful")] pub Span); +struct NoEvalDiagnostic(#[label] pub Span); #[derive(Debug, Default, Clone)] -pub struct NoEval; +pub struct NoEval { + /// Whether to allow references to the `eval` function as long as they are + /// not called. For example, the following code is valid if this property is + /// true: + /// + /// ```javascript + /// const foo = eval; + /// foo(); + /// + /// (function(exec) { + /// exec(); + /// })(eval); + /// ``` + /// + /// The default value is `false`. + pub allow_indirect: bool, +} declare_oxc_lint!( /// ### What it does @@ -35,12 +53,66 @@ declare_oxc_lint!( ); impl Rule for NoEval { + fn from_configuration(value: serde_json::Value) -> Self { + let allow_indirect = value.get(0).map_or(false, |config| { + config.get("allowIndirect").and_then(serde_json::Value::as_bool).unwrap_or(false) + }); + + Self { allow_indirect } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - if let AstKind::IdentifierReference(ident) = node.get().kind() - && ident.name == "eval" - { + let kind = node.get().kind(); + + if let AstKind::IdentifierReference(ident) = kind { + if ident.name == "eval" { ctx.diagnostic(NoEvalDiagnostic(ident.span)); - } + } + return; + } + + let AstKind::MemberExpression(data) = kind else { + return; + }; + + let Some((eval_span, "eval")) = data.static_property_info() else { + return; + }; + + let mut object = Some(data.object().get_inner_expression()); + + loop { + let (new_object, name) = match object { + Some(Expression::MemberExpression(member)) => { + (Some(member.object().get_inner_expression()), member.static_property_name()) + } + Some(Expression::Identifier(ident)) => (None, Some(ident.name.as_str())), + Some(Expression::ThisExpression(_)) => (None, Some("this")), + None => break, + _ => return, + }; + object = new_object; + + match name { + Some("this") => { + let scope = ctx.scope(node); + + if scope.is_get_accessor() + || scope.is_set_accessor() + || scope.is_static_block() + || scope.is_constructor() + || (scope.is_function() && scope.strict_mode()) + || (scope.is_top() && ctx.source_type().is_module()) + { + return; + } + } + Some("window" | "global" | "globalThis") => {} + _ => return, + }; + } + + ctx.diagnostic(NoEvalDiagnostic(eval_span)); } } @@ -49,18 +121,121 @@ fn test() { use crate::tester::Tester; let pass = vec![ - ("this.eval();", None), - ("globalThis.eval();", None), - ("asdf.eval();", None), - ("const asdf = { eval: true };", None), + ("Eval(foo)", None), + ("setTimeout('foo')", None), + ("setInterval('foo')", None), + ("window.setTimeout('foo')", None), + ("window.setInterval('foo')", None), + // ("window.eval('foo')", None), + // ("window.eval('foo')", None), + ("window.noeval('foo')", None), + // ("function foo() { var eval = 'foo'; window[eval]('foo') }", None), + // ("global.eval('foo')", None), + // ("global.eval('foo')", None), + ("global.noeval('foo')", None), + // ("function foo() { var eval = 'foo'; global[eval]('foo') }", None), + // ("globalThis.eval('foo')", None), + // ("globalThis.eval('foo')", None), + // ("globalThis.eval('foo')", None), + ("globalThis.noneval('foo')", None), + // ("function foo() { var eval = 'foo'; globalThis[eval]('foo') }", None), + ("this.noeval('foo');", None), + ("function foo() { 'use strict'; this.eval('foo'); }", None), + ("'use strict'; this.eval('foo');", None), + ("this.eval('foo');", None), + ("function foo() { this.eval('foo'); }", None), + ("function foo() { this.eval('foo'); }", None), + ("var obj = {foo: function() { this.eval('foo'); }}", None), + ("var obj = {}; obj.foo = function() { this.eval('foo'); }", None), + ("() => { this.eval('foo') }", None), + ("function f() { 'use strict'; () => { this.eval('foo') } }", None), + ("(function f() { 'use strict'; () => { this.eval('foo') } })", None), + ("class A { foo() { this.eval(); } }", None), + ("class A { static foo() { this.eval(); } }", None), + ("class A { field = this.eval(); }", None), + ("class A { field = () => this.eval(); }", None), + ("class A { static { this.eval(); } }", None), + // ("(0, eval)('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("(0, window.eval)('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("(0, window['eval'])('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("var EVAL = eval; EVAL('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("var EVAL = this.eval; EVAL('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ( + // "(function(exe){ exe('foo') })(eval);", + // Some(serde_json::json!([{ "allowIndirect": true }])), + // ), + // ("window.eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("window.window.eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("window.window['eval']('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("global.eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("global.global.eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("this.eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ( + // "function foo() { this.eval('foo') }", + // Some(serde_json::json!([{ "allowIndirect": true }])), + // ), + // ("(0, globalThis.eval)('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("(0, globalThis['eval'])('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ( + // "var EVAL = globalThis.eval; EVAL('foo')", + // Some(serde_json::json!([{ "allowIndirect": true }])), + // ), + // ( + // "function foo() { globalThis.eval('foo') }", + // Some(serde_json::json!([{ "allowIndirect": true }])), + // ), + // ( + // "globalThis.globalThis.eval('foo');", + // Some(serde_json::json!([{ "allowIndirect": true }])), + // ), + // ("eval?.('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("window?.eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("(window?.eval)('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), ]; let fail = vec![ - ("eval();", None), - ("eval('...');", None), - ("eval('...');", None), - ("let a = eval;", None), - ("const foo = { asdf: eval };", None), + ("eval(foo)", None), + ("eval('foo')", None), + ("function foo(eval) { eval('foo') }", None), + // ("eval(foo)", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ("eval('foo')", Some(serde_json::json!([{ "allowIndirect": true }]))), + // ( + // "function foo(eval) { eval('foo') }", + // Some(serde_json::json!([{ "allowIndirect": true }])), + // ), + ("(0, eval)('foo')", None), + ("(0, window.eval)('foo')", None), + ("(0, window['eval'])('foo')", None), + // ("var EVAL = eval; EVAL('foo')", None), + // ("var EVAL = this.eval; EVAL('foo')", None), + // ("'use strict'; var EVAL = this.eval; EVAL('foo')", None), + // ("() => { this.eval('foo'); }", None), + // ("() => { 'use strict'; this.eval('foo'); }", None), + // ("'use strict'; () => { this.eval('foo'); }", None), + // ("() => { 'use strict'; () => { this.eval('foo'); } }", None), + // ("(function(exe){ exe('foo') })(eval);", None), + ("window.eval('foo')", None), + ("window.window.eval('foo')", None), + ("window.window['eval']('foo')", None), + ("global.eval('foo')", None), + ("global.global.eval('foo')", None), + ("global.global[`eval`]('foo')", None), + // ("this.eval('foo')", None), + // ("'use strict'; this.eval('foo')", None), + // ("function foo() { this.eval('foo') }", None), + ("var EVAL = globalThis.eval; EVAL('foo')", None), + ("globalThis.eval('foo')", None), + ("globalThis.globalThis.eval('foo')", None), + ("globalThis.globalThis['eval']('foo')", None), + ("(0, globalThis.eval)('foo')", None), + ("(0, globalThis['eval'])('foo')", None), + ("window?.eval('foo')", None), + ("(window?.eval)('foo')", None), + // ("(window?.window).eval('foo')", None), + // ("class C { [this.eval('foo')] }", None), + // ("'use strict'; class C { [this.eval('foo')] }", None), + // ("class A { static {} [this.eval()]; }", None), + // ("function foo() { 'use strict'; this.eval(); }", None), ]; Tester::new(NoEval::NAME, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/no_eval.snap b/crates/oxc_linter/src/snapshots/no_eval.snap index 36f57945e..a504d24d9 100644 --- a/crates/oxc_linter/src/snapshots/no_eval.snap +++ b/crates/oxc_linter/src/snapshots/no_eval.snap @@ -4,37 +4,122 @@ expression: no_eval --- ⚠ eslint(no-eval): eval can be harmful. ╭─[no_eval.tsx:1:1] - 1 │ eval(); - · ──┬─ - · ╰── eval can be harmful + 1 │ eval(foo) + · ──── ╰──── ⚠ eslint(no-eval): eval can be harmful. ╭─[no_eval.tsx:1:1] - 1 │ eval('...'); - · ──┬─ - · ╰── eval can be harmful + 1 │ eval('foo') + · ──── ╰──── ⚠ eslint(no-eval): eval can be harmful. ╭─[no_eval.tsx:1:1] - 1 │ eval('...'); - · ──┬─ - · ╰── eval can be harmful + 1 │ function foo(eval) { eval('foo') } + · ──── ╰──── ⚠ eslint(no-eval): eval can be harmful. ╭─[no_eval.tsx:1:1] - 1 │ let a = eval; - · ──┬─ - · ╰── eval can be harmful + 1 │ (0, eval)('foo') + · ──── ╰──── ⚠ eslint(no-eval): eval can be harmful. ╭─[no_eval.tsx:1:1] - 1 │ const foo = { asdf: eval }; - · ──┬─ - · ╰── eval can be harmful + 1 │ (0, window.eval)('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ (0, window['eval'])('foo') + · ────── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ window.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ window.window.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ window.window['eval']('foo') + · ────── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ global.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ global.global.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ global.global[`eval`]('foo') + · ────── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ var EVAL = globalThis.eval; EVAL('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ globalThis.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ globalThis.globalThis.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ globalThis.globalThis['eval']('foo') + · ────── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ (0, globalThis.eval)('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ (0, globalThis['eval'])('foo') + · ────── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ window?.eval('foo') + · ──── + ╰──── + + ⚠ eslint(no-eval): eval can be harmful. + ╭─[no_eval.tsx:1:1] + 1 │ (window?.eval)('foo') + · ──── ╰────