Skip to content

Commit

Permalink
feat(eslint-plugin): add no-implied-eval (#1375)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk authored and bradzacher committed Jan 2, 2020
1 parent 9344233 commit 254d276
Show file tree
Hide file tree
Showing 6 changed files with 998 additions and 0 deletions.
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
95 changes: 95 additions & 0 deletions packages/eslint-plugin/docs/rules/no-implied-eval.md
@@ -0,0 +1,95 @@
# 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

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

or using `new Function()`

```ts
const fn = new Function('a', 'b', 'return a + b');
```

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.

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

## Rule Details

This rule aims to eliminate implied `eval()` through the use of `new Function()`, `setTimeout()`, `setInterval()`, `setImmediate()` or `execScript()`.

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

const fn = new Function('a', 'b', 'return a + b');
```

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 `new Function()` or `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
141 changes: 141 additions & 0 deletions packages/eslint-plugin/src/rules/no-implied-eval.ts
@@ -0,0 +1,141 @@
import * as ts from 'typescript';
import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

const FUNCTION_CONSTRUCTOR = 'Function';
const EVAL_LIKE_METHODS = new Set([
'setImmediate',
'setInterval',
'setTimeout',
'execScript',
]);

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.',
noFunctionConstructor:
'Implied eval. Do not use the Function constructor to create functions.',
},
schema: [],
type: 'suggestion',
},
defaultOptions: [],
create(context) {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

function getCalleeName(
node: TSESTree.LeftHandSideExpression,
): string | null {
if (node.type === AST_NODE_TYPES.Identifier) {
return node.name;
}

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

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

return null;
}

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

function checkImpliedEval(
node: TSESTree.NewExpression | TSESTree.CallExpression,
): void {
const calleeName = getCalleeName(node.callee);
if (calleeName === null) {
return;
}

if (calleeName === FUNCTION_CONSTRUCTOR) {
context.report({ node, messageId: 'noFunctionConstructor' });
return;
}

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

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

return {
NewExpression: checkImpliedEval,
CallExpression: checkImpliedEval,
};
},
});

0 comments on commit 254d276

Please sign in to comment.