feat(linter): improve eslint(no-eval) compat (#419)

This commit is contained in:
Carter Snook 2023-06-09 22:39:54 -05:00 committed by GitHub
parent 3263f2b654
commit 9df1545602
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 311 additions and 31 deletions

View file

@ -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)

View file

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

View file

@ -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')
· ────
╰────