Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add no-implied-eval (#1375)
- Loading branch information
1 parent
9344233
commit 254d276
Showing
6 changed files
with
998 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; | ||
}, | ||
}); |
Oops, something went wrong.