Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add no-implied-eval #1375

Merged
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -125,6 +125,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | |
| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: |
| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon:|
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallows magic numbers | | | |
| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | |
Expand Down
93 changes: 93 additions & 0 deletions packages/eslint-plugin/docs/rules/no-implied-eval.md
@@ -0,0 +1,93 @@
# Disallow the use of `eval()`-like methods (`@typescript-eslint/no-implied-eval`)

It's considered a good practice to avoid using `eval()`. There are security and performance implications involved with doing so, which is why many linters recommend disallowing `eval()`. However, there are some other ways to pass a string and have it interpreted as JavaScript code that have similar concerns.

The first is using `setTimeout()`, `setInterval()`, `setImmediate` or `execScript()` (Internet Explorer only), all of which can accept a string of code as their first argument. For example:

```ts
setTimeout('alert(`Hi!`);', 100);
```

This is considered an implied `eval()` because a string of code is
passed in to be interpreted. The same can be done with `setInterval()`, `setImmediate()` and `execScript()`. All interpret the JavaScript code in the global scope. For `setTimeout()`, `setInterval()`, `setImmediate()` and `execScript()`, the first argument can also be a function, and that is considered safer and is more performant:

```ts
setTimeout(function() {
alert('Hi!');
}, 100);
```

The best practice is to always use a function for the first argument of `setTimeout()`, `setInterval()` and `setImmediate()` (and avoid `execScript()`).

## Rule Details

This rule aims to eliminate implied `eval()` through the use of `setTimeout()`, `setInterval()`, `setImmediate()` or `execScript()`. As such, it will warn when either function is used with a string as the first argument.

Examples of **incorrect** code for this rule:

```ts
/* eslint @typescript-eslint/no-implied-eval: "error" */

setTimeout('alert(`Hi!`);', 100);

setInterval('alert(`Hi!`);', 100);

setImmediate('alert(`Hi!`)');

execScript('alert(`Hi!`)');

window.setTimeout('count = 5', 10);

window.setInterval('foo = bar', 10);

const fn = '() = {}';
setTimeout(fn, 100);

const fn = () => {
return 'x = 10';
};
setTimeout(fn(), 100);
```

Examples of **correct** code for this rule:

```ts
/* eslint @typescript-eslint/no-implied-eval: "error" */

setTimeout(function() {
alert('Hi!');
}, 100);

setInterval(function() {
alert('Hi!');
}, 100);

setImmediate(function() {
alert('Hi!');
});

execScript(function() {
alert('Hi!');
});

const fn = () => {};
setTimeout(fn, 100);

const foo = {
fn: function() {},
};
setTimeout(foo.fn, 100);
setTimeout(foo.fn.bind(this), 100);

class Foo {
static fn = () => {};
}

setTimeout(Foo.fn, 100);
```

## When Not To Use It

If you want to allow `setTimeout()`, `setInterval()`, `setImmediate()` and `execScript()` with string arguments, then you can safely disable this rule.

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-implied-eval.md)</sup>
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -39,6 +39,7 @@
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-inferrable-types": "error",
"no-magic-numbers": "off",
"@typescript-eslint/no-magic-numbers": "error",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -28,6 +28,7 @@ import noExtraSemi from './no-extra-semi';
import noExtraneousClass from './no-extraneous-class';
import noFloatingPromises from './no-floating-promises';
import noForInArray from './no-for-in-array';
import noImpliedEval from './no-implied-eval';
import noInferrableTypes from './no-inferrable-types';
import noMagicNumbers from './no-magic-numbers';
import noMisusedNew from './no-misused-new';
Expand Down Expand Up @@ -107,6 +108,7 @@ export default {
'no-floating-promises': noFloatingPromises,
'no-for-in-array': noForInArray,
'no-inferrable-types': noInferrableTypes,
'no-implied-eval': noImpliedEval,
'no-magic-numbers': noMagicNumbers,
'no-misused-new': noMisusedNew,
'no-misused-promises': noMisusedPromises,
Expand Down
123 changes: 123 additions & 0 deletions packages/eslint-plugin/src/rules/no-implied-eval.ts
@@ -0,0 +1,123 @@
import ts from 'typescript';
a-tarasyuk marked this conversation as resolved.
Show resolved Hide resolved
import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

const EVAL_LIKE_METHODS = new Set([
'setImmediate',
'setInterval',
'setTimeout',
'execScript',
a-tarasyuk marked this conversation as resolved.
Show resolved Hide resolved
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
]);

export default util.createRule({
name: 'no-implied-eval',
meta: {
docs: {
description: 'Disallow the use of `eval()`-like methods',
category: 'Best Practices',
recommended: false,
requiresTypeChecking: true,
},
messages: {
noImpliedEvalError: 'Implied eval. Consider passing a function.',
},
schema: [],
type: 'suggestion',
},
defaultOptions: [],
create(context) {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

function isEvalLikeMethod(node: TSESTree.CallExpression): boolean {
const callee = node.callee;

if (callee.type === AST_NODE_TYPES.Identifier) {
return EVAL_LIKE_METHODS.has(callee.name);
}

if (
callee.type === AST_NODE_TYPES.MemberExpression &&
callee.object.type === AST_NODE_TYPES.Identifier &&
callee.object.name === 'window'
) {
if (callee.property.type === AST_NODE_TYPES.Identifier) {
return EVAL_LIKE_METHODS.has(callee.property.name);
}

if (
callee.property.type === AST_NODE_TYPES.Literal &&
typeof callee.property.value === 'string'
) {
return EVAL_LIKE_METHODS.has(callee.property.value);
}
}

return false;
}

function isFunctionType(node: TSESTree.Node): boolean {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(tsNode);
const symbol = type.getSymbol();

if (
symbol?.flags === ts.SymbolFlags.Function ||
symbol?.flags === ts.SymbolFlags.Method
) {
return true;
}

const signatures = checker.getSignaturesOfType(
type,
ts.SignatureKind.Call,
);

if (signatures?.length) {
a-tarasyuk marked this conversation as resolved.
Show resolved Hide resolved
const [{ declaration }] = signatures;
return declaration?.kind === ts.SyntaxKind.FunctionType;
}

return false;
}

function isFunction(node: TSESTree.Node): boolean {
switch (node.type) {
case AST_NODE_TYPES.ArrowFunctionExpression:
case AST_NODE_TYPES.FunctionDeclaration:
case AST_NODE_TYPES.FunctionExpression:
return true;

case AST_NODE_TYPES.MemberExpression:
case AST_NODE_TYPES.Identifier:
return isFunctionType(node);

case AST_NODE_TYPES.CallExpression:
return (
(node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name === 'bind') ||
isFunctionType(node)
);

default:
return false;
}
}

return {
CallExpression(node): void {
if (node.arguments.length === 0) {
return;
}

const [handler] = node.arguments;
if (isEvalLikeMethod(node) && !isFunction(handler)) {
context.report({ node: handler, messageId: 'noImpliedEvalError' });
}
},
};
},
});