From 75d9908d9794da73944b804247319f9140ef0012 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 16 Mar 2021 19:05:41 +0900 Subject: [PATCH] Add es/no-function-prototype-bind rule --- docs/rules/README.md | 1 + docs/rules/no-function-prototype-bind.md | 39 +++ lib/configs/no-new-in-es5.js | 1 + lib/index.js | 1 + lib/rules/no-function-prototype-bind.js | 40 +++ lib/util/define-prototype-method-handler.js | 59 +++- tests/lib/rules/no-function-prototype-bind.js | 269 ++++++++++++++++++ 7 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 docs/rules/no-function-prototype-bind.md create mode 100644 lib/rules/no-function-prototype-bind.js create mode 100644 tests/lib/rules/no-function-prototype-bind.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 67aa97f0..de34882c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -189,6 +189,7 @@ There are multiple configs that enable all rules in this category: `plugin:es/no | [es/no-array-prototype-reduceright](./no-array-prototype-reduceright.md) | disallow the `Array.prototype.reduceRight` method. | | | [es/no-array-prototype-some](./no-array-prototype-some.md) | disallow the `Array.prototype.some` method. | | | [es/no-date-now](./no-date-now.md) | disallow the `Date.now` method. | | +| [es/no-function-prototype-bind](./no-function-prototype-bind.md) | disallow the `Function.prototype.bind` method. | | | [es/no-json](./no-json.md) | disallow the `JSON` class. | | | [es/no-keyword-properties](./no-keyword-properties.md) | disallow reserved words as property names. | | | [es/no-object-defineproperties](./no-object-defineproperties.md) | disallow the `Object.defineProperties` method. | | diff --git a/docs/rules/no-function-prototype-bind.md b/docs/rules/no-function-prototype-bind.md new file mode 100644 index 00000000..e26021a9 --- /dev/null +++ b/docs/rules/no-function-prototype-bind.md @@ -0,0 +1,39 @@ +# es/no-function-prototype-bind +> disallow the `Function.prototype.bind` method + +- ✅ The following configurations enable this rule: `plugin:es/no-new-in-es5` and `plugin:es/restrict-to-es3` + +This rule reports ES5 `Function.prototype.bind` method as errors. + +This rule is silent by default because it's hard to know types. You need to configure [the aggressive mode](../#the-aggressive-mode) or TypeScript in order to enable this rule. + +## 💡 Examples + +⛔ Examples of **incorrect** code for this rule: + + + +## 🔧 Options + +This rule has an option. + +```yml +rules: + es/no-function-prototype-bind: [error, { aggressive: false }] +``` + +### aggressive: boolean + +Configure the aggressive mode for only this rule. +This is prior to the `settings.es.aggressive` setting. + +## 📚 References + +- [Rule source](https://github.com/mysticatea/eslint-plugin-es/blob/v4.1.0/lib/rules/no-function-prototype-bind.js) +- [Test source](https://github.com/mysticatea/eslint-plugin-es/blob/v4.1.0/tests/lib/rules/no-function-prototype-bind.js) diff --git a/lib/configs/no-new-in-es5.js b/lib/configs/no-new-in-es5.js index a3f92e71..fb7457a4 100644 --- a/lib/configs/no-new-in-es5.js +++ b/lib/configs/no-new-in-es5.js @@ -19,6 +19,7 @@ module.exports = { "es/no-array-prototype-reduceright": "error", "es/no-array-prototype-some": "error", "es/no-date-now": "error", + "es/no-function-prototype-bind": "error", "es/no-json": "error", "es/no-keyword-properties": "error", "es/no-object-defineproperties": "error", diff --git a/lib/index.js b/lib/index.js index 5091fc9e..bced20e6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -88,6 +88,7 @@ module.exports = { "no-exponential-operators": require("./rules/no-exponential-operators"), "no-export-ns-from": require("./rules/no-export-ns-from"), "no-for-of-loops": require("./rules/no-for-of-loops"), + "no-function-prototype-bind": require("./rules/no-function-prototype-bind"), "no-generators": require("./rules/no-generators"), "no-global-this": require("./rules/no-global-this"), "no-import-meta": require("./rules/no-import-meta"), diff --git a/lib/rules/no-function-prototype-bind.js b/lib/rules/no-function-prototype-bind.js new file mode 100644 index 00000000..e1ca9db7 --- /dev/null +++ b/lib/rules/no-function-prototype-bind.js @@ -0,0 +1,40 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +"use strict" + +const { + definePrototypeMethodHandler, +} = require("../util/define-prototype-method-handler") + +module.exports = { + meta: { + docs: { + description: "disallow the `Function.prototype.bind` method.", + category: "ES5", + recommended: false, + url: + "http://mysticatea.github.io/eslint-plugin-es/rules/no-function-prototype-bind.html", + }, + fixable: null, + messages: { + forbidden: "ES5 '{{name}}' method is forbidden.", + }, + schema: [ + { + type: "object", + properties: { + aggressive: { type: "boolean" }, + }, + additionalProperties: false, + }, + ], + type: "problem", + }, + create(context) { + return definePrototypeMethodHandler(context, { + Function: ["bind"], + }) + }, +} diff --git a/lib/util/define-prototype-method-handler.js b/lib/util/define-prototype-method-handler.js index fb24f8ab..96134e79 100644 --- a/lib/util/define-prototype-method-handler.js +++ b/lib/util/define-prototype-method-handler.js @@ -50,6 +50,12 @@ function definePrototypeMethodHandler(context, nameMap) { ) { return className === "String" } + if ( + memberAccessNode.object.type === "FunctionExpression" || + memberAccessNode.object.type === "ArrowFunctionExpression" + ) { + return className === "Function" + } // Test object type. return isTS @@ -125,10 +131,22 @@ function definePrototypeMethodHandler(context, nameMap) { // ) // .map(([id]) => id) // .join("|"), + // "symbol.flags": !type.symbol + // ? undefined + // : Object.entries(ts.SymbolFlags) + // .filter( + // ([_id, flag]) => + // typeof flag === "number" && + // (type.symbol.flags & flag) === flag, + // ) + // .map(([id]) => id) + // .join("|"), // }, // className, // ) - + if (isFunction(type)) { + return className === "Function" + } if (isAny(type) || isUnknown(type)) { return aggressive } @@ -159,12 +177,28 @@ function definePrototypeMethodHandler(context, nameMap) { } if (isClassOrInterface(type)) { - const name = type.symbol.escapedName - return name === className || name === `Readonly${className}` + return typeSymbolEscapedNameEquals(type, className) } return checker.typeToString(type) === className } + /** + * Check if the symbol.escapedName of the given type is expected or not. + * @param {import("typescript").InterfaceType} type The type to check. + * @param {string} className The expected type name. + * @returns {boolean} `true` if should disallow it. + */ + function typeSymbolEscapedNameEquals(type, className) { + const escapedName = type.symbol.escapedName + return ( + escapedName === className || + // ReadonlyArray, ReadonlyMap, ReadonlySet + escapedName === `Readonly${className}` || + // CallableFunction + (className === "Function" && escapedName === "CallableFunction") + ) + } + /** * Get the constraint type of a given type parameter type if exists. * @@ -351,4 +385,23 @@ function isUnknown(type) { return (type.flags & ts.TypeFlags.Unknown) !== 0 } +/** + * Check if a given type is `function` or not. + * @param {import("typescript").Type} type The type to check. + * @returns {boolean} `true` if the type is `function`. + */ +function isFunction(type) { + if ( + type.symbol && + (type.symbol.flags & + (ts.SymbolFlags.Function | ts.SymbolFlags.Method)) !== + 0 + ) { + return true + } + + const signatures = type.getCallSignatures() + return signatures.length > 0 +} + module.exports = { definePrototypeMethodHandler } diff --git a/tests/lib/rules/no-function-prototype-bind.js b/tests/lib/rules/no-function-prototype-bind.js new file mode 100644 index 00000000..f956b55c --- /dev/null +++ b/tests/lib/rules/no-function-prototype-bind.js @@ -0,0 +1,269 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +"use strict" + +const path = require("path") +const RuleTester = require("../../tester") +const rule = require("../../../lib/rules/no-function-prototype-bind.js") +const ruleId = "no-function-prototype-bind" + +new RuleTester().run(ruleId, rule, { + valid: [ + "bind(this)", + "foo.bind(this)", + "(function fn(){}).name", + "(()=>{}).name", + { code: "bind(this)", settings: { es: { aggressive: true } } }, + { + code: "(function fn(){}).name", + settings: { es: { aggressive: true } }, + }, + { code: "(()=>{}).name", settings: { es: { aggressive: true } } }, + { + code: "foo.bind(this)", + options: [{ aggressive: false }], + settings: { es: { aggressive: true } }, + }, + { + code: "(function fn(){}).name", + options: [{ aggressive: false }], + settings: { es: { aggressive: true } }, + }, + { + code: "(()=>{}).name", + options: [{ aggressive: false }], + settings: { es: { aggressive: true } }, + }, + ], + invalid: [ + { + code: "(function fn(){}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "(()=>{}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + { + code: "(function fn(){}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + { + code: "(()=>{}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + { + code: "foo.bind(this)", + options: [{ aggressive: true }], + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: false } }, + }, + { + code: "(function fn(){}).bind(this)", + options: [{ aggressive: true }], + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: false } }, + }, + { + code: "(()=>{}).bind(this)", + options: [{ aggressive: true }], + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: false } }, + }, + ], +}) + +// ----------------------------------------------------------------------------- +// TypeScript +// ----------------------------------------------------------------------------- +const parser = require.resolve("@typescript-eslint/parser") +const tsconfigRootDir = path.resolve(__dirname, "../../fixtures") +const project = "tsconfig.json" +const filename = path.join(tsconfigRootDir, "test.ts") + +new RuleTester({ parser }).run(`${ruleId} TS`, rule, { + valid: [ + "bind(this)", + "foo.bind(this)", + "(function fn(){}).name", + "(()=>{}).name", + "let foo = {}; foo.bind(this)", + { + code: "bind(this)", + settings: { es: { aggressive: true } }, + }, + + // `Function` is unknown type if tsconfig.json is not configured. + "Object.assign.bind(this)", + "let foo = Function(); foo.bind(this)", + "let foo = String; foo.bind(this)", + ], + invalid: [ + { + code: "(function fn(){}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "(()=>{}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "let foo = function () {} ; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "let foo = () => {} ; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "function foo () {} ; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "function f(a: () => number) { a.bind(this) }", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "let foo = { fn () {} } ; foo.fn.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "class Foo {fn()}; const foo = new Foo(); foo.fn.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: + "function f T)>(a: T) { a.bind(this) }", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: + "function f T) | 'union'>(a: T) { a.bind(this) }", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + code: "Object.assign.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + { + code: "let foo = Function(); foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + { + code: "let foo = String; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + { + code: "foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + ], +}) + +new RuleTester({ parser, parserOptions: { tsconfigRootDir, project } }).run( + `${ruleId} TS Full Types`, + rule, + { + valid: [ + { filename, code: "bind(this)" }, + { filename, code: "foo.bind(this)" }, + { filename, code: "(function fn(){}).name" }, + { filename, code: "(()=>{}).name" }, + { filename, code: "let foo = {}; foo.bind(this)" }, + { + filename, + code: "bind(this)", + settings: { es: { aggressive: true } }, + }, + ], + invalid: [ + { + filename, + code: "(function fn(){}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "(()=>{}).bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "let foo = function () {} ; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "let foo = () => {} ; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "function foo () {} ; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "function f(a: () => number) { a.bind(this) }", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "let foo = { fn () {} } ; foo.fn.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "Object.assign.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: + "class Foo {fn()}; const foo = new Foo(); foo.fn.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "let foo = Function(); foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "let foo = String; foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: + "function f T)>(a: T) { a.bind(this) }", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: + "function f T) | 'union'>(a: T) { a.bind(this) }", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + }, + { + filename, + code: "foo.bind(this)", + errors: ["ES5 'Function.prototype.bind' method is forbidden."], + settings: { es: { aggressive: true } }, + }, + ], + }, +)