Skip to content

Commit

Permalink
feat: add es-roikoren/no-function-prototype-bind rule
Browse files Browse the repository at this point in the history
  • Loading branch information
roikoren755 committed Nov 10, 2021
1 parent 2238048 commit 0c05740
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/slimy-moons-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-es-roikoren': patch
---

feat: add `es-roikoren/no-function-prototype-bind` rule
3 changes: 2 additions & 1 deletion docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,10 @@ There are multiple configs that enable all rules in this category: `plugin:es-ro
| [es-roikoren/no-array-prototype-reduceright](./no-array-prototype-reduceright.md) | disallow the `Array.prototype.reduceRight` method. | |
| [es-roikoren/no-array-prototype-some](./no-array-prototype-some.md) | disallow the `Array.prototype.some` method. | |
| [es-roikoren/no-date-now](./no-date-now.md) | disallow the `Date.now` method. | |
| [es-roikoren/no-function-prototype-bind](./no-function-prototype-bind.md) | disallow the `Function.prototype.bind` method. | |
| [es-roikoren/no-json](./no-json.md) | disallow the `JSON` class. | |
| [es-roikoren/no-keyword-properties](./no-keyword-properties.md) | disallow reserved words as property names. | |
| [es-roikoren/no-object-create](./no-object-create.md) | disallow the `Object.create` method | |
| [es-roikoren/no-object-create](./no-object-create.md) | disallow the `Object.create` method. | |
| [es-roikoren/no-object-defineproperties](./no-object-defineproperties.md) | disallow the `Object.defineProperties` method. | |
| [es-roikoren/no-object-defineproperty](./no-object-defineproperty.md) | disallow the `Object.defineProperty` method. | |
| [es-roikoren/no-object-freeze](./no-object-freeze.md) | disallow the `Object.freeze` method. | |
Expand Down
24 changes: 24 additions & 0 deletions docs/rules/no-function-prototype-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# es-roikoren/no-function-prototype-bind
> disallow the `Function.prototype.bind` method.
- ✅ The following configurations enable this rule: `plugin:es-roikoren/no-new-in-es5` and `plugin:es-roikoren/restrict-to-es3`

This rule reports ES5 `Function.prototype.bind` method as errors.

## Examples

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

```js
/*eslint es-roikoren/no-function-prototype-bind: error */
foo.bind(this);

var foo = (function() {
return this.bar;
}).bind(this);
```

## 📚 References

- [Rule source](https://github.com/roikoren755/eslint-plugin-es/blob/v0.0.3/src/rules/no-function-prototype-bind.ts)
- [Test source](https://github.com/roikoren755/eslint-plugin-es/blob/v0.0.3/tests/src/rules/no-function-prototype-bind.ts)
2 changes: 1 addition & 1 deletion docs/rules/no-object-create.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# es-roikoren/no-object-create
> disallow the `Object.create` method
> disallow the `Object.create` method.
- ✅ The following configurations enable this rule: `plugin:es-roikoren/no-new-in-es5` and `plugin:es-roikoren/restrict-to-es3`

Expand Down
1 change: 1 addition & 0 deletions src/configs/no-new-in-es5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default {
'es-roikoren/no-array-prototype-reduceright': 'error',
'es-roikoren/no-array-prototype-some': 'error',
'es-roikoren/no-date-now': 'error',
'es-roikoren/no-function-prototype-bind': 'error',
'es-roikoren/no-json': 'error',
'es-roikoren/no-keyword-properties': 'error',
'es-roikoren/no-object-create': 'error',
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import noDynamicImport from './rules/no-dynamic-import';
import noExponentialOperators from './rules/no-exponential-operators';
import noExportNsFrom from './rules/no-export-ns-from';
import noForOfLoops from './rules/no-for-of-loops';
import noFunctionPrototypeBind from './rules/no-function-prototype-bind';
import noGenerators from './rules/no-generators';
import noGlobalThis from './rules/no-global-this';
import noImportMeta from './rules/no-import-meta';
Expand Down Expand Up @@ -223,6 +224,7 @@ export default {
'no-exponential-operators': noExponentialOperators,
'no-export-ns-from': noExportNsFrom,
'no-for-of-loops': noForOfLoops,
'no-function-prototype-bind': noFunctionPrototypeBind,
'no-generators': noGenerators,
'no-global-this': noGlobalThis,
'no-import-meta': noImportMeta,
Expand Down
18 changes: 18 additions & 0 deletions src/rules/no-function-prototype-bind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createRule } from '../util/create-rule';
import { definePrototypeMethodHandler, schema } from '../util/define-prototype-method-handler';
import type { IAggressive } from '../util/define-prototype-method-handler';

export const category = 'ES5';
export default createRule<[options: IAggressive], 'forbidden'>({
name: 'no-function-prototype-bind',
meta: {
type: 'problem',
docs: { description: 'disallow the `Function.prototype.bind` method.', recommended: false },
schema,
messages: { forbidden: "ES5 '{{name}}' method is forbidden." },
},
defaultOptions: [{}],
create(context) {
return definePrototypeMethodHandler(context, { Function: ['bind'] });
},
});
2 changes: 1 addition & 1 deletion src/rules/no-object-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default createRule<[], 'forbidden'>({
name: 'no-object-create',
meta: {
type: 'problem',
docs: { description: 'disallow the `Object.create` method', recommended: false },
docs: { description: 'disallow the `Object.create` method.', recommended: false },
schema: [],
messages: { forbidden: "ES5 '{{name}}' method is forbidden." },
},
Expand Down
49 changes: 46 additions & 3 deletions src/util/define-prototype-method-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,40 @@ const isUnionOrIntersection = (type: TypeScript.Type): type is TypeScript.UnionO
*/
const isUnknown = (type: TypeScript.Type): boolean => (type.flags & (ts as TS).TypeFlags.Unknown) !== 0; // eslint-disable-line no-bitwise

/**
* Check if a given type is `function` or not.
* @param {TypeScript.Type} type The type to check.
* @returns {boolean} `true` if the type is `function`.
*/
const isFunction = (type: TypeScript.Type): boolean => {
// eslint-disable-next-line no-bitwise
if (type.symbol && (type.symbol.flags & ((ts as TS).SymbolFlags.Function | (ts as TS).SymbolFlags.Method)) !== 0) {
return true;
}

const signatures = type.getCallSignatures();

return signatures.length > 0;
};

/**
* Check if the symbol.escapedName of the given type is expected or not.
* @param {TypeScript.InterfaceType} type The type to check.
* @param {string} className The expected type name.
* @returns {boolean} `true` if should disallow it.
*/
const typeSymbolEscapedNameEquals = (type: TypeScript.InterfaceType, className: string): boolean => {
const { escapedName } = type.symbol;

return (
escapedName === className ||
// ReadonlyArray, ReadonlyMap, ReadonlySet
escapedName === `Readonly${className}` ||
// CallableFunction
(className === 'Function' && escapedName === 'CallableFunction')
);
};

interface IOptions {
aggressive: boolean;
checker?: TypeScript.TypeChecker | undefined;
Expand Down Expand Up @@ -162,6 +196,10 @@ const getConstraintType = (
* @returns {boolean} `true` if should disallow it.
*/
const typeEquals = (type: TypeScript.Type, className: string, options: IOptions): boolean => {
if (isFunction(type)) {
return className === 'Function';
}

if (isAny(type) || isUnknown(type)) {
return options.aggressive;
}
Expand Down Expand Up @@ -198,9 +236,7 @@ const typeEquals = (type: TypeScript.Type, className: string, options: IOptions)
}

if (isClassOrInterface(type)) {
const name = type.symbol.escapedName;

return name === className || name === `Readonly${className}`;
return typeSymbolEscapedNameEquals(type, className);
}

return options.checker?.typeToString(type) === className;
Expand Down Expand Up @@ -277,6 +313,13 @@ const checkObjectType = (memberAccessNode: TSESTree.MemberExpression, className:
return className === 'String';
}

if (
memberAccessNode.object.type === 'FunctionExpression' ||
memberAccessNode.object.type === 'ArrowFunctionExpression'
) {
return className === 'Function';
}

// Test object type.
return options.isTS
? checkByPropertyDeclaration(memberAccessNode, className, options) ||
Expand Down
140 changes: 140 additions & 0 deletions tests/src/rules/no-function-prototype-bind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import path from 'path';
import { AST_NODE_TYPES } from '@typescript-eslint/types';

import { RuleTester } from '../../tester';
import rule from '../../../src/rules/no-function-prototype-bind';

const ruleId = 'no-function-prototype-bind';
const error = {
messageId: 'forbidden' as const,
line: 1,
column: 1,
type: AST_NODE_TYPES.MemberExpression,
data: { name: '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: [{ ...error, column: 1 }] },
{ code: '(()=>{}).bind(this)', errors: [{ ...error, column: 1 }] },
{ code: 'foo.bind(this)', errors: [{ ...error, column: 1 }], settings: { es: { aggressive: true } } },
{ code: '(function fn(){}).bind(this)', errors: [{ ...error, column: 1 }], settings: { es: { aggressive: true } } },
{ code: '(()=>{}).bind(this)', errors: [{ ...error, column: 1 }], settings: { es: { aggressive: true } } },
{
code: 'foo.bind(this)',
options: [{ aggressive: true }],
errors: [{ ...error, column: 1 }],
settings: { es: { aggressive: false } },
},
{
code: '(function fn(){}).bind(this)',
options: [{ aggressive: true }],
errors: [{ ...error, column: 1 }],
settings: { es: { aggressive: false } },
},
{
code: '(()=>{}).bind(this)',
options: [{ aggressive: true }],
errors: [{ ...error, column: 1 }],
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: [{ ...error, column: 1 }] },
{ code: '(()=>{}).bind(this)', errors: [{ ...error, column: 1 }] },
{ code: 'let foo = function () {}; foo.bind(this)', errors: [{ ...error, column: 27 }] },
{ code: 'let foo = () => {}; foo.bind(this)', errors: [{ ...error, column: 21 }] },
{ code: 'function foo () {}; foo.bind(this)', errors: [{ ...error, column: 21 }] },
{ code: 'function f(a: () => number) { a.bind(this) }', errors: [{ ...error, column: 31 }] },
{ code: 'let foo = { fn () {} }; foo.fn.bind(this)', errors: [{ ...error, column: 25 }] },
{ code: 'class Foo {fn()}; const foo = new Foo(); foo.fn.bind(this)', errors: [{ ...error, column: 42 }] },
{ code: 'function f<T extends ((a: any) => T)>(a: T) { a.bind(this) }', errors: [{ ...error, column: 47 }] },
{
code: "function f<T extends ((a: any) => T) | 'union'>(a: T) { a.bind(this) }",
errors: [{ ...error, column: 57 }],
},
{ code: 'Object.assign.bind(this)', errors: [{ ...error, column: 1 }], settings: { es: { aggressive: true } } },
{
code: 'let foo = Function(); foo.bind(this)',
errors: [{ ...error, column: 23 }],
settings: { es: { aggressive: true } },
},
{
code: 'let foo = String; foo.bind(this)',
errors: [{ ...error, column: 19 }],
settings: { es: { aggressive: true } },
},
{ code: 'foo.bind(this)', errors: [{ ...error, column: 1 }], 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: [{ ...error, column: 1 }] },
{ filename, code: '(()=>{}).bind(this)', errors: [{ ...error, column: 1 }] },
{ filename, code: 'let foo = function () {}; foo.bind(this)', errors: [{ ...error, column: 27 }] },
{ filename, code: 'let foo = () => {}; foo.bind(this)', errors: [{ ...error, column: 21 }] },
{ filename, code: 'function foo () {}; foo.bind(this)', errors: [{ ...error, column: 21 }] },
{ filename, code: 'function f(a: () => number) { a.bind(this) }', errors: [{ ...error, column: 31 }] },
{ filename, code: 'let foo = { fn () {} }; foo.fn.bind(this)', errors: [{ ...error, column: 25 }] },
{ filename, code: 'Object.assign.bind(this)', errors: [{ ...error, column: 1 }] },
{ filename, code: 'class Foo {fn()}; const foo = new Foo(); foo.fn.bind(this)', errors: [{ ...error, column: 42 }] },
{ filename, code: 'let foo = Function(); foo.bind(this)', errors: [{ ...error, column: 23 }] },
{ filename, code: 'let foo = String; foo.bind(this)', errors: [{ ...error, column: 19 }] },
{
filename,
code: 'function f<T extends ((a: any) => T)>(a: T) { a.bind(this) }',
errors: [{ ...error, column: 47 }],
},
{
filename,
code: "function f<T extends ((a: any) => T) | 'union'>(a: T) { a.bind(this) }",
errors: [{ ...error, column: 57 }],
},
{ filename, code: 'foo.bind(this)', errors: [{ ...error, column: 1 }], settings: { es: { aggressive: true } } },
],
});

0 comments on commit 0c05740

Please sign in to comment.